mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-15 19:27:56 +01:00
UITestAutomation Framework (#37461)
* Add UITestAutomation framework * add code comments * Optimized code format * Optimized code format * Update commons and add keyboard manager ui test project * Optimized code format * test scope and fix fancyzone exe path * Add readme * Optimize helper functions and UI test method * Fix spelling errors and restore module UI tests * Restore Indent * Update NOTICE.md * Update comments to Session and Elements * Update comments for Button and Window * delete unnecessary code * change FindElementByName to FindElmenet * Update comments for ModuleConfigData * Update readme and comments * Remove extra comments * change public property * Optimize code readability * add default Attach Function * change attach function name * Update comments to XML format * Hide by internal functions * Update readme * Refine the framework * Fix process start position and update readme * Remove Enum PowerToysModuleWindow * Update attach comments * Update ModuleConfigData comments --------- Co-authored-by: Zhaopeng Wang (from Dev Box) <zhaopengwang@microsoft.com> Co-authored-by: Xiaofeng Wang (from Dev Box) <xiaofengwang@microsoft.com> Co-authored-by: urnotdfs <709586527@qq.com>
This commit is contained in:
@@ -1322,6 +1322,7 @@ EXHIBIT A -Mozilla Public License.
|
|||||||
- Microsoft.CodeAnalysis.NetAnalyzers 9.0.0
|
- Microsoft.CodeAnalysis.NetAnalyzers 9.0.0
|
||||||
- Microsoft.Data.Sqlite 9.0.2
|
- Microsoft.Data.Sqlite 9.0.2
|
||||||
- Microsoft.Diagnostics.Tracing.TraceEvent 3.1.16
|
- Microsoft.Diagnostics.Tracing.TraceEvent 3.1.16
|
||||||
|
- Microsoft.DotNet.ILCompiler (A)
|
||||||
- Microsoft.Extensions.DependencyInjection 9.0.2
|
- Microsoft.Extensions.DependencyInjection 9.0.2
|
||||||
- Microsoft.Extensions.Hosting 9.0.2
|
- Microsoft.Extensions.Hosting 9.0.2
|
||||||
- Microsoft.Extensions.Hosting.WindowsServices 9.0.2
|
- Microsoft.Extensions.Hosting.WindowsServices 9.0.2
|
||||||
|
|||||||
@@ -636,6 +636,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ZoomItModuleInterface", "sr
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ZoomItSettingsInterop", "src\modules\ZoomIt\ZoomItSettingsInterop\ZoomItSettingsInterop.vcxproj", "{CA7D8106-30B9-4AEC-9D05-B69B31B8C461}"
|
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ZoomItSettingsInterop", "src\modules\ZoomIt\ZoomItSettingsInterop\ZoomItSettingsInterop.vcxproj", "{CA7D8106-30B9-4AEC-9D05-B69B31B8C461}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UITestAutomation", "src\common\UITestAutomation\UITestAutomation.csproj", "{A558C25D-2007-498E-8B6F-43405AFAE9E2}"
|
||||||
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KeyboardManagerEditorUI", "src\modules\keyboardmanager\KeyboardManagerEditorUI\KeyboardManagerEditorUI.csproj", "{08F9155D-B6DC-46E5-9C83-AF60B655898B}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KeyboardManagerEditorUI", "src\modules\keyboardmanager\KeyboardManagerEditorUI\KeyboardManagerEditorUI.csproj", "{08F9155D-B6DC-46E5-9C83-AF60B655898B}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "KeyboardManagerEditorLibraryWrapper", "src\modules\keyboardmanager\KeyboardManagerEditorLibraryWrapper\KeyboardManagerEditorLibraryWrapper.vcxproj", "{4382A954-179A-4078-92AF-715187DFFF50}"
|
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "KeyboardManagerEditorLibraryWrapper", "src\modules\keyboardmanager\KeyboardManagerEditorLibraryWrapper\KeyboardManagerEditorLibraryWrapper.vcxproj", "{4382A954-179A-4078-92AF-715187DFFF50}"
|
||||||
@@ -2250,6 +2252,20 @@ Global
|
|||||||
{CA7D8106-30B9-4AEC-9D05-B69B31B8C461}.Release|ARM64.Build.0 = Release|ARM64
|
{CA7D8106-30B9-4AEC-9D05-B69B31B8C461}.Release|ARM64.Build.0 = Release|ARM64
|
||||||
{CA7D8106-30B9-4AEC-9D05-B69B31B8C461}.Release|x64.ActiveCfg = Release|x64
|
{CA7D8106-30B9-4AEC-9D05-B69B31B8C461}.Release|x64.ActiveCfg = Release|x64
|
||||||
{CA7D8106-30B9-4AEC-9D05-B69B31B8C461}.Release|x64.Build.0 = Release|x64
|
{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
|
||||||
{08F9155D-B6DC-46E5-9C83-AF60B655898B}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
{08F9155D-B6DC-46E5-9C83-AF60B655898B}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||||
{08F9155D-B6DC-46E5-9C83-AF60B655898B}.Debug|ARM64.Build.0 = Debug|ARM64
|
{08F9155D-B6DC-46E5-9C83-AF60B655898B}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||||
{08F9155D-B6DC-46E5-9C83-AF60B655898B}.Debug|x64.ActiveCfg = Debug|x64
|
{08F9155D-B6DC-46E5-9C83-AF60B655898B}.Debug|x64.ActiveCfg = Debug|x64
|
||||||
@@ -2509,6 +2525,9 @@ Global
|
|||||||
{0A84F764-3A88-44CD-AA96-41BDBD48627B} = {DD6E12FE-5509-4ABC-ACC2-3D6DC98A238C}
|
{0A84F764-3A88-44CD-AA96-41BDBD48627B} = {DD6E12FE-5509-4ABC-ACC2-3D6DC98A238C}
|
||||||
{E4585179-2AC1-4D5F-A3FF-CFC5392F694C} = {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}
|
{CA7D8106-30B9-4AEC-9D05-B69B31B8C461} = {DD6E12FE-5509-4ABC-ACC2-3D6DC98A238C}
|
||||||
|
{A558C25D-2007-498E-8B6F-43405AFAE9E2} = {1AFB6476-670D-4E80-A464-657E01DFF482}
|
||||||
|
{08F9155D-B6DC-46E5-9C83-AF60B655898B} = {38BDB927-829B-4C65-9CD9-93FB05D66D65}
|
||||||
|
{4382A954-179A-4078-92AF-715187DFFF50} = {38BDB927-829B-4C65-9CD9-93FB05D66D65}
|
||||||
{EBED240C-8702-452D-B764-6DB9DA9179AF} = {F05E590D-AD46-42BE-9C25-6A63ADD2E3EA}
|
{EBED240C-8702-452D-B764-6DB9DA9179AF} = {F05E590D-AD46-42BE-9C25-6A63ADD2E3EA}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
|||||||
91
doc/devdocs/UITests.md
Normal file
91
doc/devdocs/UITests.md
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# 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:
|
||||||
|
|
||||||
|
>Specify Scope:
|
||||||
|
```
|
||||||
|
[TestClass]
|
||||||
|
public class RunFancyZonesTest : UITestBase
|
||||||
|
{
|
||||||
|
public RunFancyZonesTest()
|
||||||
|
: base(PowerToysModule.FancyZone)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Then you can start using session to perform the UI operations.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
```
|
||||||
|
using Microsoft.PowerToys.UITest;
|
||||||
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
|
|
||||||
|
namespace UITests_KeyboardManager
|
||||||
|
{
|
||||||
|
[TestClass]
|
||||||
|
public class RunKeyboardManagerUITests : UITestBase
|
||||||
|
{
|
||||||
|
[TestMethod]
|
||||||
|
public void OpenKeyboardManagerEditor()
|
||||||
|
{
|
||||||
|
// Open KeyboardManagerEditor
|
||||||
|
this.Session.Find<Button>(By.Name("Remap a key")).Click();
|
||||||
|
this.Session.Attach("Remap keys");
|
||||||
|
|
||||||
|
// Maximize window
|
||||||
|
var window = Session.Find<Window>(By.Name("Remap keys")).Maximize();
|
||||||
|
|
||||||
|
// Add Key Remapping
|
||||||
|
this.Session.Find<Button>(By.Name("Add key remapping")).Click();
|
||||||
|
window.Close();
|
||||||
|
|
||||||
|
// Back to Settings
|
||||||
|
this.Session.Attach(PowerToysModule.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)
|
||||||
20
src/common/UITestAutomation/Element/Button.cs
Normal file
20
src/common/UITestAutomation/Element/Button.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// 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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a button in the UI test environment.
|
||||||
|
/// </summary>
|
||||||
|
public class Button : Element
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
69
src/common/UITestAutomation/Element/By.cs
Normal file
69
src/common/UITestAutomation/Element/By.cs
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
// 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 static OpenQA.Selenium.By;
|
||||||
|
|
||||||
|
namespace Microsoft.PowerToys.UITest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This class represents a By selector.
|
||||||
|
/// </summary>
|
||||||
|
public class By
|
||||||
|
{
|
||||||
|
private readonly OpenQA.Selenium.By by;
|
||||||
|
|
||||||
|
private By(OpenQA.Selenium.By by)
|
||||||
|
{
|
||||||
|
this.by = by;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a By object using the name attribute.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The name attribute to search for.</param>
|
||||||
|
/// <returns>A By object.</returns>
|
||||||
|
public static By Name(string name) => new By(OpenQA.Selenium.By.Name(name));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a By object using the ID attribute.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The ID attribute to search for.</param>
|
||||||
|
/// <returns>A By object.</returns>
|
||||||
|
public static By Id(string id) => new By(OpenQA.Selenium.By.Id(id));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a By object using the XPath expression.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="xpath">The XPath expression to search for.</param>
|
||||||
|
/// <returns>A By object.</returns>
|
||||||
|
public static By XPath(string xpath) => new By(OpenQA.Selenium.By.XPath(xpath));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a By object using the CSS selector.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cssSelector">The CSS selector to search for.</param>
|
||||||
|
/// <returns>A By object.</returns>
|
||||||
|
public static By CssSelector(string cssSelector) => new By(OpenQA.Selenium.By.CssSelector(cssSelector));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a By object using the link text.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="linkText">The link text to search for.</param>
|
||||||
|
/// <returns>A By object.</returns>
|
||||||
|
public static By LinkText(string linkText) => new By(OpenQA.Selenium.By.LinkText(linkText));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a By object using the tag name.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tagName">The tag name to search for.</param>
|
||||||
|
/// <returns>A By object.</returns>
|
||||||
|
public static By TagName(string tagName) => new By(OpenQA.Selenium.By.TagName(tagName));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts the By object to an OpenQA.Selenium.By object.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>An OpenQA.Selenium.By object.</returns>
|
||||||
|
internal OpenQA.Selenium.By ToSeleniumBy() => by;
|
||||||
|
}
|
||||||
|
}
|
||||||
185
src/common/UITestAutomation/Element/Element.cs
Normal file
185
src/common/UITestAutomation/Element/Element.cs
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
// 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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a basic UI element in the application.
|
||||||
|
/// </summary>
|
||||||
|
public class Element
|
||||||
|
{
|
||||||
|
private WindowsElement? windowsElement;
|
||||||
|
private WindowsDriver<WindowsElement>? driver;
|
||||||
|
|
||||||
|
internal void SetWindowsElement(WindowsElement windowsElement) => this.windowsElement = windowsElement;
|
||||||
|
|
||||||
|
internal void SetSession(WindowsDriver<WindowsElement> driver) => this.driver = driver;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the name of the UI element.
|
||||||
|
/// </summary>
|
||||||
|
public string Name
|
||||||
|
{
|
||||||
|
get { return GetAttribute("Name"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the text of the UI element.
|
||||||
|
/// </summary>
|
||||||
|
public string Text
|
||||||
|
{
|
||||||
|
get { return GetAttribute("Value"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the AutomationID of the UI element.
|
||||||
|
/// </summary>
|
||||||
|
public string AutomationId
|
||||||
|
{
|
||||||
|
get { return GetAttribute("AutomationId"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the class name of the UI element.
|
||||||
|
/// </summary>
|
||||||
|
public string ClassName
|
||||||
|
{
|
||||||
|
get { return GetAttribute("ClassName"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the help text of the UI element.
|
||||||
|
/// </summary>
|
||||||
|
public string HelpText
|
||||||
|
{
|
||||||
|
get { return GetAttribute("HelpText"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the control type of the UI element.
|
||||||
|
/// </summary>
|
||||||
|
public string ControlType
|
||||||
|
{
|
||||||
|
get { return GetAttribute("ControlType"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the UI element is enabled.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>True if the element is enabled; otherwise, false.</returns>
|
||||||
|
public bool IsEnabled() => GetAttribute("IsEnabled") == "True";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the UI element is selected.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>True if the element is selected; otherwise, false.</returns>
|
||||||
|
public bool IsSelected() => GetAttribute("IsSelected") == "True";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Click the UI element.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="rightClick">If true, performs a right-click; otherwise, performs a left-click.</param>
|
||||||
|
public void Click(bool rightClick = false)
|
||||||
|
{
|
||||||
|
PerformAction(actions =>
|
||||||
|
{
|
||||||
|
if (rightClick)
|
||||||
|
{
|
||||||
|
actions.ContextClick();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
actions.Click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the attribute value of the UI element.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="attributeName">The name of the attribute to get.</param>
|
||||||
|
/// <returns>The value of the attribute.</returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds an element by the selector.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The class type of the element to find.</typeparam>
|
||||||
|
/// <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 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 = FindElementHelper.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds all elements by the selector.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The class type of the elements to find.</typeparam>
|
||||||
|
/// <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)
|
||||||
|
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 = FindElementHelper.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Simulates a manual operation on the element.
|
||||||
|
/// </summary>
|
||||||
|
private void PerformAction(Action<Actions> action)
|
||||||
|
{
|
||||||
|
var element = this.windowsElement;
|
||||||
|
Actions actions = new Actions(this.driver);
|
||||||
|
actions.MoveToElement(element);
|
||||||
|
action(actions);
|
||||||
|
actions.Build().Perform();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
60
src/common/UITestAutomation/Element/FindElementHelper.cs
Normal file
60
src/common/UITestAutomation/Element/FindElementHelper.cs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// 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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Helper class for finding elements.
|
||||||
|
/// </summary>
|
||||||
|
internal static class FindElementHelper
|
||||||
|
{
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
var items = findElementsFunc();
|
||||||
|
var res = items.Select(item =>
|
||||||
|
{
|
||||||
|
var element = item as WindowsElement;
|
||||||
|
return NewElement<T>(element, driver, timeoutMS);
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
return new ReadOnlyCollection<T>(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static T NewElement<T>(WindowsElement? element, WindowsDriver<WindowsElement>? driver, int timeoutMS)
|
||||||
|
where T : Element, new()
|
||||||
|
{
|
||||||
|
Assert.IsNotNull(driver, $"New Element {typeof(T).Name} error: driver is null.");
|
||||||
|
Assert.IsNotNull(element, $"New Element {typeof(T).Name} error: element is null.");
|
||||||
|
|
||||||
|
T newElement = new T();
|
||||||
|
driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromMilliseconds(timeoutMS);
|
||||||
|
newElement.SetSession(driver);
|
||||||
|
newElement.SetWindowsElement(element);
|
||||||
|
return newElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
92
src/common/UITestAutomation/Element/Window.cs
Normal file
92
src/common/UITestAutomation/Element/Window.cs
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
// 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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a window in the UI test environment.
|
||||||
|
/// </summary>
|
||||||
|
public class Window : Element
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Maximizes the window.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="byClickButton">If true, clicks the Maximize button; otherwise, sets the window state.</param>
|
||||||
|
/// <returns>The current Window instance.</returns>
|
||||||
|
public Window Maximize(bool byClickButton = true)
|
||||||
|
{
|
||||||
|
if (byClickButton)
|
||||||
|
{
|
||||||
|
Find<Button>(By.Name("Maximize")).Click();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// TODO: Implement maximizing the window using an alternative method
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Restores the window.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="byClickButton">If true, clicks the Restore button; otherwise, sets the window state.</param>
|
||||||
|
/// <returns>The current Window instance.</returns>
|
||||||
|
public Window Restore(bool byClickButton = true)
|
||||||
|
{
|
||||||
|
if (byClickButton)
|
||||||
|
{
|
||||||
|
Find<Button>(By.Name("Restore")).Click();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// TODO: Implement restoring the window using an alternative method
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Minimizes the window.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="byClickButton">If true, clicks the Minimize button; otherwise, sets the window state.</param>
|
||||||
|
/// <returns>The current Window instance.</returns>
|
||||||
|
public Window Minimize(bool byClickButton = true)
|
||||||
|
{
|
||||||
|
if (byClickButton)
|
||||||
|
{
|
||||||
|
Find<Button>(By.Name("Minimize")).Click();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// TODO: Implement minimizing the window using an alternative method
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Closes the window.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="byClickButton">If true, clicks the Close button; otherwise, closes the window using an alternative method.</param>
|
||||||
|
public void Close(bool byClickButton = true)
|
||||||
|
{
|
||||||
|
if (byClickButton)
|
||||||
|
{
|
||||||
|
Find<Button>(By.Name("Close")).Click();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// TODO: Implement closing the window using an alternative method
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
72
src/common/UITestAutomation/ModuleConfigData.cs
Normal file
72
src/common/UITestAutomation/ModuleConfigData.cs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
// 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.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
[assembly: InternalsVisibleTo("UITestBase")]
|
||||||
|
[assembly: InternalsVisibleTo("Session")]
|
||||||
|
|
||||||
|
namespace Microsoft.PowerToys.UITest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This file manages the configuration of modules for UI tests.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// How to add a new module:
|
||||||
|
/// 1. Define the new module in the PowerToysModule enum.
|
||||||
|
/// 2. Add the exe window name to the ModuleWindowName dictionary in the ModuleConfigData constructor.
|
||||||
|
/// 3. Add the exe path to the ModulePath dictionary in the ModuleConfigData constructor.
|
||||||
|
/// </remarks>
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the modules in PowerToys.
|
||||||
|
/// </summary>
|
||||||
|
public enum PowerToysModule
|
||||||
|
{
|
||||||
|
PowerToysSettings,
|
||||||
|
FancyZone,
|
||||||
|
Hosts,
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class ModuleConfigData
|
||||||
|
{
|
||||||
|
private Dictionary<PowerToysModule, string> ModulePath { get; }
|
||||||
|
|
||||||
|
// Singleton instance of ModuleConfigData.
|
||||||
|
private static readonly Lazy<ModuleConfigData> SingletonInstance = new Lazy<ModuleConfigData>(() => new ModuleConfigData());
|
||||||
|
|
||||||
|
public static ModuleConfigData Instance => SingletonInstance.Value;
|
||||||
|
|
||||||
|
public const string WindowsApplicationDriverUrl = "http://127.0.0.1:4723";
|
||||||
|
|
||||||
|
public Dictionary<PowerToysModule, string> ModuleWindowName { get; }
|
||||||
|
|
||||||
|
private ModuleConfigData()
|
||||||
|
{
|
||||||
|
// The exe window name for each module.
|
||||||
|
ModuleWindowName = new Dictionary<PowerToysModule, string>
|
||||||
|
{
|
||||||
|
[PowerToysModule.PowerToysSettings] = "PowerToys Settings",
|
||||||
|
[PowerToysModule.FancyZone] = "FancyZones Layout",
|
||||||
|
[PowerToysModule.Hosts] = "Hosts File Editor",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Exe start path for the module if it exists.
|
||||||
|
ModulePath = new Dictionary<PowerToysModule, string>
|
||||||
|
{
|
||||||
|
[PowerToysModule.PowerToysSettings] = @"\..\..\..\WinUI3Apps\PowerToys.Settings.exe",
|
||||||
|
[PowerToysModule.FancyZone] = @"\..\..\..\PowerToys.FancyZonesEditor.exe",
|
||||||
|
[PowerToysModule.Hosts] = @"\..\..\..\WinUI3Apps\PowerToys.Hosts.exe",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetModulePath(PowerToysModule scope) => ModulePath[scope];
|
||||||
|
|
||||||
|
public string GetWindowsApplicationDriverUrl() => WindowsApplicationDriverUrl;
|
||||||
|
|
||||||
|
public string GetModuleWindowName(PowerToysModule scope) => ModuleWindowName[scope];
|
||||||
|
}
|
||||||
|
}
|
||||||
129
src/common/UITestAutomation/Session.cs
Normal file
129
src/common/UITestAutomation/Session.cs
Normal file
@@ -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.Collections.ObjectModel;
|
||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Xml.Linq;
|
||||||
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
|
using OpenQA.Selenium;
|
||||||
|
using OpenQA.Selenium.Appium;
|
||||||
|
using OpenQA.Selenium.Appium.Windows;
|
||||||
|
using OpenQA.Selenium.Interactions;
|
||||||
|
|
||||||
|
namespace Microsoft.PowerToys.UITest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides interfaces for interacting with UI elements.
|
||||||
|
/// </summary>
|
||||||
|
public class Session
|
||||||
|
{
|
||||||
|
private WindowsDriver<WindowsElement> Root { get; set; }
|
||||||
|
|
||||||
|
private WindowsDriver<WindowsElement> WindowsDriver { get; set; }
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern bool SetForegroundWindow(nint hWnd);
|
||||||
|
|
||||||
|
public Session(WindowsDriver<WindowsElement> root, WindowsDriver<WindowsElement> windowsDriver)
|
||||||
|
{
|
||||||
|
this.Root = root;
|
||||||
|
this.WindowsDriver = windowsDriver;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds an element by selector.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The class of the element, should be Element or its derived class.</typeparam>
|
||||||
|
/// <param name="by">The selector to find the element.</param>
|
||||||
|
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
|
||||||
|
/// <returns>The found element.</returns>
|
||||||
|
public T Find<T>(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 = FindElementHelper.Find<T, WindowsElement>(
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
var element = this.WindowsDriver.FindElement(by.ToSeleniumBy());
|
||||||
|
Assert.IsNotNull(element, $"Element not found using selector: {by}");
|
||||||
|
return element;
|
||||||
|
},
|
||||||
|
this.WindowsDriver,
|
||||||
|
timeoutMS);
|
||||||
|
|
||||||
|
return foundElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds all elements by selector.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The class of the elements, should be Element or its derived class.</typeparam>
|
||||||
|
/// <param name="by">The selector to find the elements.</param>
|
||||||
|
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
|
||||||
|
/// <returns>A read-only collection of the found elements.</returns>
|
||||||
|
public ReadOnlyCollection<T>? FindAll<T>(By by, int timeoutMS = 3000)
|
||||||
|
where T : Element, new()
|
||||||
|
{
|
||||||
|
Assert.IsNotNull(this.WindowsDriver, $"WindowsElement is null in method FindAll<{typeof(T).Name}> with parameters: by = {by}, timeoutMS = {timeoutMS}");
|
||||||
|
var foundElements = FindElementHelper.FindAll<T, WindowsElement>(
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
var elements = this.WindowsDriver.FindElements(by.ToSeleniumBy());
|
||||||
|
Assert.IsTrue(elements.Count > 0, $"Elements not found using selector: {by}");
|
||||||
|
return elements;
|
||||||
|
},
|
||||||
|
this.WindowsDriver,
|
||||||
|
timeoutMS);
|
||||||
|
|
||||||
|
return foundElements;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attaches to an existing PowerToys module.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="module">The PowerToys module to attach to.</param>
|
||||||
|
/// <returns>The attached session.</returns>
|
||||||
|
public Session Attach(PowerToysModule module)
|
||||||
|
{
|
||||||
|
string windowName = ModuleConfigData.Instance.GetModuleWindowName(module);
|
||||||
|
return this.Attach(windowName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attaches to an existing exe by string window name.
|
||||||
|
/// The session should be attached when a new app is started.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="windowName">The window name to attach to.</param>
|
||||||
|
/// <returns>The attached session.</returns>
|
||||||
|
public Session Attach(string windowName)
|
||||||
|
{
|
||||||
|
if (this.Root != null)
|
||||||
|
{
|
||||||
|
var window = this.Root.FindElementByName(windowName);
|
||||||
|
Assert.IsNotNull(window, $"Failed to attach. Window '{windowName}' not found");
|
||||||
|
|
||||||
|
var windowHandle = new nint(int.Parse(window.GetAttribute("NativeWindowHandle")));
|
||||||
|
SetForegroundWindow(windowHandle);
|
||||||
|
var hexWindowHandle = windowHandle.ToString("x");
|
||||||
|
var appCapabilities = new AppiumOptions();
|
||||||
|
appCapabilities.AddAdditionalCapability("appTopLevelWindow", hexWindowHandle);
|
||||||
|
appCapabilities.AddAdditionalCapability("deviceName", "WindowsPC");
|
||||||
|
this.WindowsDriver = new WindowsDriver<WindowsElement>(new Uri(ModuleConfigData.Instance.GetWindowsApplicationDriverUrl()), appCapabilities);
|
||||||
|
Assert.IsNotNull(this.WindowsDriver, "Attach WindowsDriver is null");
|
||||||
|
|
||||||
|
// Set implicit timeout to make element search retry every 500 ms
|
||||||
|
this.WindowsDriver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(3);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Assert.IsNotNull(this.Root, $"Failed to attach to the window '{windowName}'. Root driver is null");
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/common/UITestAutomation/UITestAutomation.csproj
Normal file
21
src/common/UITestAutomation/UITestAutomation.csproj
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Library</OutputType>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<PublishAot>true</PublishAot>
|
||||||
|
<InvariantGlobalization>true</InvariantGlobalization>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Appium.WebDriver" />
|
||||||
|
<PackageReference Include="MSTest" />
|
||||||
|
<PackageReference Include="System.IO.Abstractions" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Properties\" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
130
src/common/UITestAutomation/UITestBase.cs
Normal file
130
src/common/UITestAutomation/UITestBase.cs
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
// 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.ObjectModel;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
|
using OpenQA.Selenium;
|
||||||
|
using OpenQA.Selenium.Appium;
|
||||||
|
using OpenQA.Selenium.Appium.Windows;
|
||||||
|
using OpenQA.Selenium.Interactions;
|
||||||
|
|
||||||
|
namespace Microsoft.PowerToys.UITest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Base class that should be inherited by all Test Classes.
|
||||||
|
/// </summary>
|
||||||
|
public class UITestBase
|
||||||
|
{
|
||||||
|
public Session Session { get; set; }
|
||||||
|
|
||||||
|
private readonly TestInit testInit = new TestInit();
|
||||||
|
|
||||||
|
public UITestBase(PowerToysModule scope = PowerToysModule.PowerToysSettings)
|
||||||
|
{
|
||||||
|
this.testInit.SetScope(scope);
|
||||||
|
this.testInit.Init();
|
||||||
|
this.Session = new Session(this.testInit.GetRoot(), this.testInit.GetDriver());
|
||||||
|
}
|
||||||
|
|
||||||
|
~UITestBase()
|
||||||
|
{
|
||||||
|
this.testInit.Cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Nested class for test initialization.
|
||||||
|
/// </summary>
|
||||||
|
private sealed class TestInit
|
||||||
|
{
|
||||||
|
private WindowsDriver<WindowsElement> Root { get; set; }
|
||||||
|
|
||||||
|
private WindowsDriver<WindowsElement>? 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<WindowsElement>(new Uri(ModuleConfigData.Instance.GetWindowsApplicationDriverUrl()), desktopCapabilities);
|
||||||
|
|
||||||
|
// Set default timeout to 5 seconds
|
||||||
|
this.Root.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the test environment.
|
||||||
|
/// </summary>
|
||||||
|
[UnconditionalSuppressMessage("SingleFile", "IL3000:Avoid accessing Assembly file path when publishing as a single file", Justification = "<Pending>")]
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cleans up the test environment.
|
||||||
|
/// </summary>
|
||||||
|
public void Cleanup()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
appDriver?.Kill();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Handle exceptions if needed
|
||||||
|
Debug.WriteLine($"Exception during Cleanup: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Starts a new exe and takes control of it.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="appPath">The path to the application executable.</param>
|
||||||
|
public void StartExe(string appPath)
|
||||||
|
{
|
||||||
|
var opts = new AppiumOptions();
|
||||||
|
opts.AddAdditionalCapability("app", appPath);
|
||||||
|
this.Driver = new WindowsDriver<WindowsElement>(new Uri(ModuleConfigData.Instance.GetWindowsApplicationDriverUrl()), opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets scope to the Test Class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="scope">The PowerToys module to start.</param>
|
||||||
|
public void SetScope(PowerToysModule scope)
|
||||||
|
{
|
||||||
|
sessionPath = ModuleConfigData.Instance.GetModulePath(scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
public WindowsDriver<WindowsElement> GetRoot() => this.Root;
|
||||||
|
|
||||||
|
public WindowsDriver<WindowsElement> GetDriver()
|
||||||
|
{
|
||||||
|
Assert.IsNotNull(this.Driver, $"Failed to get driver. Driver is null.");
|
||||||
|
return this.Driver;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user