Skip to content

Commit 3bc472b

Browse files
committed
initial commit
1 parent 582f3eb commit 3bc472b

24 files changed

Lines changed: 2351 additions & 9 deletions

PowerToys.slnx

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99
<Platform Solution="*|ARM64" Project="ARM64" />
1010
<Platform Solution="*|x64" Project="x64" />
1111
</Project>
12-
<Project Path="src/common/Common.UI/Common.UI.csproj">
12+
<Project Path="src/common/Common.UI.Controls/Common.UI.Controls.csproj">
1313
<Platform Solution="*|ARM64" Project="ARM64" />
1414
<Platform Solution="*|x64" Project="x64" />
1515
</Project>
16-
<Project Path="src/common/Common.UI.Controls/Common.UI.Controls.csproj">
16+
<Project Path="src/common/Common.UI/Common.UI.csproj">
1717
<Platform Solution="*|ARM64" Project="ARM64" />
1818
<Platform Solution="*|x64" Project="x64" />
1919
</Project>
@@ -54,10 +54,14 @@
5454
<Platform Solution="*|ARM64" Project="ARM64" />
5555
<Platform Solution="*|x64" Project="x64" />
5656
</Project>
57+
<Project Path="src/common/UITestAutomation.Next/UITestAutomation.Next.csproj">
58+
<Platform Solution="*|ARM64" Project="ARM64" />
59+
<Platform Solution="*|x64" Project="x64" />
60+
</Project>
5761
<Project Path="src/common/UnitTests-CommonLib/UnitTests-CommonLib.vcxproj" Id="1a066c63-64b3-45f8-92fe-664e1cce8077" />
5862
<Project Path="src/common/UnitTests-CommonUtils/UnitTests-CommonUtils.vcxproj" Id="8b5cfb38-ccba-40a8-ad7a-89c57b070884" />
59-
<Project Path="src/common/updating/updating.vcxproj" Id="17da04df-e393-4397-9cf0-84dabe11032e" />
6063
<Project Path="src/common/updating/UnitTests/UpdatingUnitTests.vcxproj" Id="a1b2c3d4-e5f6-7890-abcd-ef1234567890" />
64+
<Project Path="src/common/updating/updating.vcxproj" Id="17da04df-e393-4397-9cf0-84dabe11032e" />
6165
<Project Path="src/common/version/version.vcxproj" Id="cc6e41ac-8174-4e8a-8d22-85dd7f4851df" />
6266
</Folder>
6367
<Folder Name="/common/interop/">
@@ -184,6 +188,10 @@
184188
<Platform Solution="*|ARM64" Project="ARM64" />
185189
<Platform Solution="*|x64" Project="x64" />
186190
</Project>
191+
<Project Path="src/modules/colorPicker/UITest-ColorPicker.Next/UITest-ColorPicker.Next.csproj">
192+
<Platform Solution="*|ARM64" Project="ARM64" />
193+
<Platform Solution="*|x64" Project="x64" />
194+
</Project>
187195
</Folder>
188196
<Folder Name="/modules/colorpicker/Tests/">
189197
<Project Path="src/modules/colorPicker/ColorPickerUI.UnitTests/ColorPickerUI.UnitTests.csproj">
@@ -200,11 +208,11 @@
200208
</Project>
201209
</Folder>
202210
<Folder Name="/modules/CommandPalette/Built-in Extensions/">
203-
<Project Path="src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Microsoft.CmdPal.Ext.Apps.csproj">
211+
<Project Path="src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Actions/Microsoft.CmdPal.Ext.Actions.csproj">
204212
<Platform Solution="*|ARM64" Project="ARM64" />
205213
<Platform Solution="*|x64" Project="x64" />
206214
</Project>
207-
<Project Path="src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Actions/Microsoft.CmdPal.Ext.Actions.csproj">
215+
<Project Path="src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Microsoft.CmdPal.Ext.Apps.csproj">
208216
<Platform Solution="*|ARM64" Project="ARM64" />
209217
<Platform Solution="*|x64" Project="x64" />
210218
</Project>
@@ -715,11 +723,11 @@
715723
</Project>
716724
</Folder>
717725
<Folder Name="/modules/PowerDisplay/">
718-
<Project Path="src/modules/powerdisplay/PowerDisplay.Models/PowerDisplay.Models.csproj">
726+
<Project Path="src/modules/powerdisplay/PowerDisplay.Lib/PowerDisplay.Lib.csproj">
719727
<Platform Solution="*|ARM64" Project="ARM64" />
720728
<Platform Solution="*|x64" Project="x64" />
721729
</Project>
722-
<Project Path="src/modules/powerdisplay/PowerDisplay.Lib/PowerDisplay.Lib.csproj">
730+
<Project Path="src/modules/powerdisplay/PowerDisplay.Models/PowerDisplay.Models.csproj">
723731
<Platform Solution="*|ARM64" Project="ARM64" />
724732
<Platform Solution="*|x64" Project="x64" />
725733
</Project>
@@ -1119,14 +1127,14 @@
11191127
<BuildDependency Project="src/modules/fancyzones/editor/FancyZonesEditor/FancyZonesEditor.csproj" />
11201128
<BuildDependency Project="src/modules/fancyzones/FancyZonesLib/FancyZonesLib.vcxproj" />
11211129
<BuildDependency Project="src/modules/fancyzones/FancyZonesModuleInterface/FancyZonesModuleInterface.vcxproj" />
1130+
<BuildDependency Project="src/modules/GrabAndMove/GrabAndMove/GrabAndMove.vcxproj" />
1131+
<BuildDependency Project="src/modules/GrabAndMove/GrabAndMoveModuleInterface/GrabAndMoveModuleInterface.vcxproj" />
11221132
<BuildDependency Project="src/modules/imageresizer/dll/ImageResizerExt.vcxproj" />
11231133
<BuildDependency Project="src/modules/imageresizer/ui/ImageResizerUI.csproj" />
11241134
<BuildDependency Project="src/modules/keyboardmanager/dll/KeyboardManager.vcxproj" />
11251135
<BuildDependency Project="src/modules/launcher/Microsoft.Launcher/Microsoft.Launcher.vcxproj" />
11261136
<BuildDependency Project="src/modules/LightSwitch/LightSwitchModuleInterface/LightSwitchModuleInterface.vcxproj" />
11271137
<BuildDependency Project="src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj" />
1128-
<BuildDependency Project="src/modules/GrabAndMove/GrabAndMoveModuleInterface/GrabAndMoveModuleInterface.vcxproj" />
1129-
<BuildDependency Project="src/modules/GrabAndMove/GrabAndMove/GrabAndMove.vcxproj" />
11301138
<BuildDependency Project="src/modules/powerrename/dll/PowerRenameExt.vcxproj" />
11311139
<BuildDependency Project="src/modules/powerrename/lib/PowerRenameLib.vcxproj" />
11321140
<BuildDependency Project="src/modules/previewpane/Common/PreviewHandlerCommon.csproj" />
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright (c) Microsoft Corporation
2+
// The Microsoft Corporation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
namespace Microsoft.PowerToys.UITest.Next;
6+
7+
/// <summary>
8+
/// Selector used to locate elements via winappcli. winappcli has its own selector grammar
9+
/// (semantic slugs, plain text search) so this type maps onto the CLI's argument shape
10+
/// rather than mimicking Selenium's <c>By</c>.
11+
/// </summary>
12+
public sealed class By
13+
{
14+
public enum Kind
15+
{
16+
/// <summary>Plain-text search against Name or AutomationId (case-insensitive substring).</summary>
17+
Text,
18+
19+
/// <summary>Stable AutomationId, when the developer set one.</summary>
20+
AutomationId,
21+
22+
/// <summary>A semantic slug (e.g., <c>btn-close-d1a0</c>) printed by <c>inspect</c>/<c>search</c>.</summary>
23+
Slug,
24+
}
25+
26+
public Kind Selector { get; }
27+
28+
public string Value { get; }
29+
30+
private By(Kind kind, string value)
31+
{
32+
Selector = kind;
33+
Value = value;
34+
}
35+
36+
/// <summary>Plain-text search; what you'd type into <c>winapp ui search "&lt;text&gt;"</c>.</summary>
37+
public static By Name(string name) => new(Kind.Text, name);
38+
39+
/// <summary>Look up by stable AutomationId (winappcli also accepts these as selectors).</summary>
40+
public static By AccessibilityId(string id) => new(Kind.AutomationId, id);
41+
42+
/// <inheritdoc cref="AccessibilityId(string)"/>
43+
public static By Id(string id) => new(Kind.AutomationId, id);
44+
45+
/// <summary>Direct slug selector (e.g., <c>btn-colorpicker-b415</c>) as printed by inspect/search.</summary>
46+
public static By Slug(string slug) => new(Kind.Slug, slug);
47+
48+
public override string ToString() => $"{Selector}={Value}";
49+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright (c) Microsoft Corporation
2+
// The Microsoft Corporation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
namespace Microsoft.PowerToys.UITest.Next;
6+
7+
public class Button : Element
8+
{
9+
public Button()
10+
{
11+
TargetControlType = "Button";
12+
}
13+
}
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
// Copyright (c) Microsoft Corporation
2+
// The Microsoft Corporation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Text.Json;
6+
using Microsoft.VisualStudio.TestTools.UnitTesting;
7+
8+
namespace Microsoft.PowerToys.UITest.Next;
9+
10+
/// <summary>
11+
/// Reference to a UI element resolved via winappcli. Wraps the resolved <see cref="Selector"/>
12+
/// (slug or text query), the owning <see cref="Session"/>, and the metadata captured at lookup
13+
/// time (control type, class name, name).
14+
/// </summary>
15+
/// <remarks>
16+
/// Element instances are <i>stateless on the wire</i> — every property read and every action
17+
/// shells out to <c>winapp ui …</c>. The cached <see cref="ControlType"/>, <see cref="ClassName"/>,
18+
/// and <see cref="Name"/> are the values seen at <c>Find</c> time; for fresh values, re-find.
19+
/// </remarks>
20+
public class Element
21+
{
22+
internal Session? Owner { get; set; }
23+
24+
/// <summary>The selector winappcli will use to address this element (semantic slug, ID, or text query).</summary>
25+
public string Selector { get; internal set; } = string.Empty;
26+
27+
/// <summary>Cached control type at lookup time (e.g. "Button", "ToggleSwitch").</summary>
28+
public string ControlType { get; internal set; } = string.Empty;
29+
30+
/// <summary>Cached class name at lookup time (e.g. "ToggleSwitch", "TextBlock").</summary>
31+
public string ClassName { get; internal set; } = string.Empty;
32+
33+
/// <summary>Cached Name property at lookup time.</summary>
34+
public string Name { get; internal set; } = string.Empty;
35+
36+
/// <summary>Top-left X (screen pixels) reported by <c>search</c> at lookup time.</summary>
37+
public int X { get; internal set; }
38+
39+
/// <summary>Top-left Y (screen pixels) reported by <c>search</c> at lookup time.</summary>
40+
public int Y { get; internal set; }
41+
42+
/// <summary>Bounding-box width reported by <c>search</c> at lookup time.</summary>
43+
public int Width { get; internal set; }
44+
45+
/// <summary>Bounding-box height reported by <c>search</c> at lookup time.</summary>
46+
public int Height { get; internal set; }
47+
48+
/// <summary>UIA control type that this wrapper subclass expects (e.g. <c>"Button"</c>). Null = match anything.</summary>
49+
protected string? TargetControlType { get; set; }
50+
51+
/// <summary>Optional ClassName filter applied alongside <see cref="TargetControlType"/>.</summary>
52+
protected string? TargetClassName { get; set; }
53+
54+
internal bool MatchesFilter()
55+
{
56+
if (TargetControlType is not null &&
57+
!string.Equals(ControlType, TargetControlType, StringComparison.OrdinalIgnoreCase))
58+
{
59+
return false;
60+
}
61+
62+
if (TargetClassName is not null &&
63+
!string.Equals(ClassName, TargetClassName, StringComparison.OrdinalIgnoreCase))
64+
{
65+
return false;
66+
}
67+
68+
return true;
69+
}
70+
71+
/// <summary>
72+
/// Activate the element. winappcli's <c>invoke</c> tries InvokePattern → TogglePattern →
73+
/// SelectionItemPattern → ExpandCollapsePattern in order; <c>rightClick</c> falls back to
74+
/// <c>click --right</c> via real mouse input.
75+
/// </summary>
76+
public virtual void Click(bool rightClick = false, int msPostAction = 200)
77+
{
78+
EnsureBound();
79+
80+
if (rightClick)
81+
{
82+
WinappCli.InvokeAssertSuccess("ui", "click", Selector, "-w", Owner!.WindowHandleArg, "--right");
83+
}
84+
else
85+
{
86+
WinappCli.InvokeAssertSuccess("ui", "invoke", Selector, "-w", Owner!.WindowHandleArg);
87+
}
88+
89+
if (msPostAction > 0)
90+
{
91+
Thread.Sleep(msPostAction);
92+
}
93+
}
94+
95+
/// <summary>
96+
/// Mouse-simulation left-click via <c>winapp ui click &lt;slug&gt;</c>. Use for elements that
97+
/// don't expose an InvokePattern (e.g. TextBlocks, ListItems, column headers), where the
98+
/// click is handled by an ancestor's Click handler rather than by the element itself.
99+
/// </summary>
100+
public void MouseClick(int msPostAction = 200)
101+
{
102+
EnsureBound();
103+
WinappCli.InvokeAssertSuccess("ui", "click", Selector, "-w", Owner!.WindowHandleArg);
104+
if (msPostAction > 0)
105+
{
106+
Thread.Sleep(msPostAction);
107+
}
108+
}
109+
110+
/// <summary>Move keyboard focus to this element.</summary>
111+
public void Focus()
112+
{
113+
EnsureBound();
114+
WinappCli.InvokeAssertSuccess("ui", "focus", Selector, "-w", Owner!.WindowHandleArg);
115+
}
116+
117+
/// <summary>
118+
/// Read a single UIA property via <c>winapp ui get-property … --json</c>. Returns the raw string
119+
/// value as winappcli reports it (e.g. <c>"On"</c>/<c>"Off"</c> for <c>ToggleState</c>).
120+
/// </summary>
121+
public string GetProperty(string propertyName)
122+
{
123+
EnsureBound();
124+
var root = WinappCli.InvokeJson("ui", "get-property", Selector, "-p", propertyName, "-w", Owner!.WindowHandleArg, "--json");
125+
if (root.TryGetProperty("properties", out var props) &&
126+
props.TryGetProperty(propertyName, out var v))
127+
{
128+
return v.GetString() ?? string.Empty;
129+
}
130+
131+
return string.Empty;
132+
}
133+
134+
/// <summary>
135+
/// UIA <c>HelpText</c> (from <c>AutomationProperties.HelpText</c>). Used by the Settings UI
136+
/// ShortcutControl to surface the current shortcut as readable text on the EditButton
137+
/// (e.g. <c>"Win + Shift + C"</c>).
138+
/// </summary>
139+
public string HelpText => GetProperty("HelpText");
140+
141+
/// <summary>
142+
/// Read the element's value via <c>winapp ui get-value … --json</c>. winappcli walks
143+
/// TextPattern → ValuePattern → SelectionPattern → Name to find a value, so this returns
144+
/// the rendered text content of TextBlocks (e.g. ColorPicker's <c>ColorTextBlock</c>
145+
/// where <c>AutomationProperties.Name</c> overrides the UIA Name with the color's friendly
146+
/// name, but the actual <c>Text</c> binding holds the HEX value we want).
147+
/// </summary>
148+
public string GetValue()
149+
{
150+
EnsureBound();
151+
var root = WinappCli.InvokeJson("ui", "get-value", Selector, "-w", Owner!.WindowHandleArg, "--json");
152+
if (root.TryGetProperty("text", out var t))
153+
{
154+
return t.GetString() ?? string.Empty;
155+
}
156+
157+
return string.Empty;
158+
}
159+
160+
/// <summary>
161+
/// Wait for this element to reach <paramref name="expectedValue"/> on <paramref name="propertyName"/>.
162+
/// Mirrors <c>winapp ui wait-for --property X --value Y -t T</c>; returns true on success, false on timeout.
163+
/// </summary>
164+
public bool WaitForProperty(string propertyName, string expectedValue, int timeoutMS = 5000)
165+
{
166+
EnsureBound();
167+
var r = WinappCli.Invoke(
168+
"ui", "wait-for", Selector,
169+
"-w", Owner!.WindowHandleArg,
170+
"--property", propertyName,
171+
"--value", expectedValue,
172+
"-t", timeoutMS.ToString(System.Globalization.CultureInfo.InvariantCulture));
173+
return r.ExitCode == 0;
174+
}
175+
176+
/// <summary>
177+
/// Wait for any element matching the original selector to disappear from the tree
178+
/// (<c>winapp ui wait-for … --gone</c>).
179+
/// </summary>
180+
public bool WaitForGone(int timeoutMS = 5000)
181+
{
182+
EnsureBound();
183+
var r = WinappCli.Invoke(
184+
"ui", "wait-for", Selector,
185+
"-w", Owner!.WindowHandleArg,
186+
"--gone",
187+
"-t", timeoutMS.ToString(System.Globalization.CultureInfo.InvariantCulture));
188+
return r.ExitCode == 0;
189+
}
190+
191+
/// <summary>Find a descendant matching <paramref name="by"/>, scoped under this element via its slug.</summary>
192+
public T Find<T>(By by, int timeoutMS = 5000)
193+
where T : Element, new()
194+
{
195+
EnsureBound();
196+
197+
// winappcli scopes a search beneath an element by passing the parent's selector to inspect.
198+
// For most cases (within the same window) the global search is fine and faster; if you need
199+
// strict scoping under a subtree, use a slug By that prefixes with the parent's slug.
200+
return Owner!.FindUnder<T>(by, timeoutMS);
201+
}
202+
203+
public T Find<T>(string name, int timeoutMS = 5000)
204+
where T : Element, new() => Find<T>(By.Name(name), timeoutMS);
205+
206+
private void EnsureBound()
207+
{
208+
Assert.IsNotNull(Owner, "Element is not bound to a Session.");
209+
Assert.IsFalse(string.IsNullOrEmpty(Selector), "Element has no selector.");
210+
}
211+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) Microsoft Corporation
2+
// The Microsoft Corporation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
namespace Microsoft.PowerToys.UITest.Next;
6+
7+
/// <summary>WinUI NavigationViewItem surfaces as ControlType.ListItem.</summary>
8+
public class NavigationViewItem : Element
9+
{
10+
public NavigationViewItem()
11+
{
12+
TargetControlType = "ListItem";
13+
}
14+
}

0 commit comments

Comments
 (0)