Merge branch 'feature/UITestAutomation' into dev/yaqingmi/ui-automation

This commit is contained in:
Yaqing Mi (from Dev Box)
2025-04-17 18:44:53 +08:00
7 changed files with 130 additions and 21 deletions

View File

@@ -57,3 +57,4 @@ stages:
platform: ${{ platform }}
configuration: Release
useLatestWebView2: ${{ parameters.useLatestWebView2 }}
isUIAutomationPipeline: true

View File

@@ -7,6 +7,7 @@ using System.Drawing;
using System.Runtime.CompilerServices;
using System.Xml.Linq;
using ABI.Windows.Foundation;
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium;
@@ -199,6 +200,42 @@ namespace Microsoft.PowerToys.UITest
});
}
/// <summary>
/// Simulates holding a key, clicking and dragging a UI element to the specified screen coordinates.
/// </summary>
/// <param name="key">The keyboard key to press and hold during the drag operation.</param>
/// <param name="targetX">The target X-coordinate to drag the element to.</param>
/// <param name="targetY">The target Y-coordinate to drag the element to.</param>
public void KeyDownAndDrag(Key key, int targetX, int targetY)
{
PerformAction((actions, windowElement) =>
{
KeyboardHelper.PressKey(key);
actions.MoveToElement(windowsElement)
.ClickAndHold()
.Perform();
int dx = targetX - windowElement.Rect.X;
int dy = targetY - windowElement.Rect.Y;
int stepCount = 10;
int stepX = dx / stepCount;
int stepY = dy / stepCount;
for (int i = 0; i < stepCount; i++)
{
var stepAction = new Actions(driver);
stepAction.MoveByOffset(stepX, stepY).Perform();
}
var releaseAction = new Actions(driver);
releaseAction.Release().Perform();
KeyboardHelper.ReleaseKey(key);
});
}
/// <summary>
/// Gets the attribute value of the UI element.
/// </summary>
@@ -365,15 +402,10 @@ namespace Microsoft.PowerToys.UITest
/// Save UI Element to a PNG file.
/// </summary>
/// <param name="path">the full path</param>
internal void SaveToPngFile(string path, bool eraseUserPreferenceColor)
internal void SaveToPngFile(string path)
{
Assert.IsNotNull(this.windowsElement, $"WindowsElement is null in method SaveToFile with parameter: path = {path}");
this.windowsElement.GetScreenshot().SaveAsFile(path);
if (eraseUserPreferenceColor)
{
VisualHelper.EraseUserPreferenceColor(path);
}
}
}
}

View File

@@ -112,6 +112,16 @@ namespace Microsoft.PowerToys.UITest
SendWinKeyCombination(keysToSend);
}
public static void PressKey(Key key)
{
PressVirtualKey(TranslateKeyHex(key));
}
public static void ReleaseKey(Key key)
{
ReleaseVirtualKey(TranslateKeyHex(key));
}
/// <summary>
        /// Translates a key to its corresponding SendKeys representation.
        /// </summary>
@@ -260,6 +270,26 @@ namespace Microsoft.PowerToys.UITest
}
}
/// <summary>
/// map the virtual key codes to the corresponding keys.
/// </summary>
private static byte TranslateKeyHex(Key key)
{
switch (key)
{
case Key.Win:
return 0x5B; // Windows Key - 0x5B in hex
case Key.Ctrl:
return 0x11; // Ctrl Key - 0x11 in hex
case Key.Alt:
return 0x12; // Alt Key - 0x12 in hex
case Key.Shift:
return 0x10; // Shift Key - 0x10 in hex
default:
throw new ArgumentException($"Key {key} is not supported, Please add your key at TranslateKeyHex for translation to hex.");
}
}
/// <summary>
/// Sends a combination of keys, including the Windows key, to the system.
/// </summary>
@@ -283,5 +313,21 @@ namespace Microsoft.PowerToys.UITest
keybd_event(VK_LWIN, 0, KEYEVENTF_KEYUP, UIntPtr.Zero);
}
}
/// <summary>
/// Just press the key.(no release)
/// </summary>
private static void PressVirtualKey(byte key)
{
keybd_event(key, 0, KEYEVENTF_KEYDOWN, UIntPtr.Zero);
}
/// <summary>
/// Release only the button (if pressed first)
/// </summary>
private static void ReleaseVirtualKey(byte key)
{
keybd_event(key, 0, KEYEVENTF_KEYUP, UIntPtr.Zero);
}
}
}

View File

@@ -439,6 +439,30 @@ namespace Microsoft.PowerToys.UITest
});
}
/// <summary>
/// release the key (after the hold key and drag is completed.)
/// </summary>
/// <param name="key">The key release.</param>
public void PressKey(Key key)
{
PerformAction(() =>
{
KeyboardHelper.PressKey(key);
});
}
/// <summary>
/// press and hold the specified key.
/// </summary>
/// <param name="key">The key to press and hold .</param>
public void ReleaseKey(Key key)
{
PerformAction(() =>
{
KeyboardHelper.ReleaseKey(key);
});
}
/// <summary>
/// Sends a sequence of keys.
/// </summary>

View File

@@ -538,7 +538,7 @@ namespace Microsoft.PowerToys.UITest
}
else
{
Console.WriteLine("virtual monitor create faild");
Console.WriteLine("virtual monitor create failed");
}
}

View File

@@ -5,6 +5,7 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.IO;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.PowerToys.UITest
@@ -20,7 +21,7 @@ namespace Microsoft.PowerToys.UITest
/// <param name="element">Element object</param>
/// <param name="scenarioSubname">additional scenario name if two or more scenarios in one test</param>
[RequiresUnreferencedCode("This method uses reflection which may not be compatible with trimming.")]
public static void AreEqual(TestContext? testContext, Element element, System.Reflection.Assembly callerAssembly, string scenarioSubname = "")
public static void AreEqual(TestContext? testContext, Element element, string scenarioSubname = "")
{
if (element == null)
{
@@ -52,8 +53,7 @@ namespace Microsoft.PowerToys.UITest
var tempTestImagePath = GetTempFilePath(scenarioSubname, "test", ".png");
// Save the image with the user preference color erased
element.SaveToPngFile(tempTestImagePath, true);
element.SaveToPngFile(tempTestImagePath);
if (string.IsNullOrEmpty(baselineImageResourceName)
|| !Path.GetFileNameWithoutExtension(baselineImageResourceName).EndsWith(scenarioSubname))
@@ -65,21 +65,27 @@ namespace Microsoft.PowerToys.UITest
bool isSame = false;
#pragma warning disable CS8604 // Possible null reference argument.
using (var baselineImage = new Bitmap(callerMethod!.DeclaringType!.Assembly.GetManifestResourceStream(baselineImageResourceName)))
using (var stream = callerMethod!.DeclaringType!.Assembly.GetManifestResourceStream(baselineImageResourceName))
{
using (var testImage = new Bitmap(tempTestImagePath))
if (stream == null)
{
isSame = VisualAssert.AreEqual(baselineImage, testImage);
Assert.Fail($"Resource stream '{baselineImageResourceName}' is null.");
}
if (!isSame)
using (var baselineImage = new Bitmap(stream))
{
using (var testImage = new Bitmap(tempTestImagePath))
{
// Copy baseline image to temp folder as well
baselineImage.Save(tempBaselineImagePath);
isSame = VisualAssert.AreEqual(baselineImage, testImage);
if (!isSame)
{
// Copy baseline image to temp folder as well
baselineImage.Save(tempBaselineImagePath);
}
}
}
}
#pragma warning restore CS8604 // Possible null reference argument.
if (!isSame)
{

View File

@@ -40,7 +40,7 @@ namespace Hosts.UITests
// 'Add an entry' button (only show-up when list is empty) should be visible
Assert.IsTrue(this.HasOne<HyperlinkButton>("Add an entry"), "'Add an entry' button should be visible in the empty view");
// VisualAssert.AreEqual(this.Find("Entries"), "EmptyView");
VisualAssert.AreEqual(this.TestContext, this.Find("Entries"), "EmptyView");
// Click 'Add an entry' from empty-view for adding Host override rule
this.Find<HyperlinkButton>("Add an entry").Click();
@@ -51,7 +51,7 @@ namespace Hosts.UITests
Assert.IsTrue(this.Has<Button>("Delete"), "Should have one row now");
Assert.IsFalse(this.Has<HyperlinkButton>("Add an entry"), "'Add an entry' button should be invisible if not empty view");
// VisualAssert.AreEqual(this.Find("Entries"), "NonEmptyView");
VisualAssert.AreEqual(this.TestContext, this.Find("Entries"), "NonEmptyView");
}
/// <summary>
@@ -74,7 +74,7 @@ namespace Hosts.UITests
Assert.IsTrue(this.Has<Button>("Delete"), "Should have one row now");
// VisualAssert.AreEqual(this.TestContext, this.Find("Entries"));
VisualAssert.AreEqual(this.TestContext, this.Find("Entries"));
}
/// <summary>