Skip to content

Commit cf3d132

Browse files
committed
move n rename
1 parent c37eaf0 commit cf3d132

14 files changed

Lines changed: 527 additions & 84 deletions

File tree

.github/actions/spell-check/expect.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2084,6 +2084,8 @@ wifi
20842084
wikimedia
20852085
wikipedia
20862086
winapi
2087+
winapp
2088+
winappcli
20872089
winappsdk
20882090
windir
20892091
WINDOWCREATED

PowerToys.slnx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -188,16 +188,16 @@
188188
<Platform Solution="*|ARM64" Project="ARM64" />
189189
<Platform Solution="*|x64" Project="x64" />
190190
</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>
195191
</Folder>
196192
<Folder Name="/modules/colorpicker/Tests/">
197193
<Project Path="src/modules/colorPicker/ColorPickerUI.UnitTests/ColorPickerUI.UnitTests.csproj">
198194
<Platform Solution="*|ARM64" Project="ARM64" />
199195
<Platform Solution="*|x64" Project="x64" />
200196
</Project>
197+
<Project Path="src/modules/colorPicker/ColorPicker.UITests/ColorPicker.UITests.csproj">
198+
<Platform Solution="*|ARM64" Project="ARM64" />
199+
<Platform Solution="*|x64" Project="x64" />
200+
</Project>
201201
</Folder>
202202
<Folder Name="/modules/CommandPalette/">
203203
<Project Path="src/modules/cmdpal/CmdPalKeyboardService/CmdPalKeyboardService.vcxproj" Id="5f63c743-f6ce-4dba-a200-2b3f8a14e8c2" />
@@ -1096,6 +1096,10 @@
10961096
<Platform Solution="*|ARM64" Project="ARM64" />
10971097
<Platform Solution="*|x64" Project="x64" />
10981098
</Project>
1099+
<Project Path="src/settings-ui/UITest-Settings.Next/UITest-Settings.Next.csproj">
1100+
<Platform Solution="*|ARM64" Project="ARM64" />
1101+
<Platform Solution="*|x64" Project="x64" />
1102+
</Project>
10991103
</Folder>
11001104
<Folder Name="/Solution Items/">
11011105
<File Path=".vsconfig" />

src/common/UITestAutomation.Next/Element/Element.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,11 @@ public virtual void Click(bool rightClick = false, int msPostAction = 200)
7979

8080
if (rightClick)
8181
{
82-
WinappCli.InvokeAssertSuccess("ui", "click", Selector, "-w", Owner!.WindowHandleArg, "--right");
82+
WinappCli.InvokeAssertSuccess("ui", "click", Selector, Owner!.TargetFlag, Owner!.TargetValue, "--right");
8383
}
8484
else
8585
{
86-
WinappCli.InvokeAssertSuccess("ui", "invoke", Selector, "-w", Owner!.WindowHandleArg);
86+
WinappCli.InvokeAssertSuccess("ui", "invoke", Selector, Owner!.TargetFlag, Owner!.TargetValue);
8787
}
8888

8989
if (msPostAction > 0)
@@ -100,7 +100,7 @@ public virtual void Click(bool rightClick = false, int msPostAction = 200)
100100
public void MouseClick(int msPostAction = 200)
101101
{
102102
EnsureBound();
103-
WinappCli.InvokeAssertSuccess("ui", "click", Selector, "-w", Owner!.WindowHandleArg);
103+
WinappCli.InvokeAssertSuccess("ui", "click", Selector, Owner!.TargetFlag, Owner!.TargetValue);
104104
if (msPostAction > 0)
105105
{
106106
Thread.Sleep(msPostAction);
@@ -111,7 +111,7 @@ public void MouseClick(int msPostAction = 200)
111111
public void Focus()
112112
{
113113
EnsureBound();
114-
WinappCli.InvokeAssertSuccess("ui", "focus", Selector, "-w", Owner!.WindowHandleArg);
114+
WinappCli.InvokeAssertSuccess("ui", "focus", Selector, Owner!.TargetFlag, Owner!.TargetValue);
115115
}
116116

117117
/// <summary>
@@ -121,7 +121,7 @@ public void Focus()
121121
public string GetProperty(string propertyName)
122122
{
123123
EnsureBound();
124-
var root = WinappCli.InvokeJson("ui", "get-property", Selector, "-p", propertyName, "-w", Owner!.WindowHandleArg, "--json");
124+
var root = WinappCli.InvokeJson("ui", "get-property", Selector, "-p", propertyName, Owner!.TargetFlag, Owner!.TargetValue, "--json");
125125
if (root.TryGetProperty("properties", out var props) &&
126126
props.TryGetProperty(propertyName, out var v))
127127
{
@@ -148,7 +148,7 @@ public string GetProperty(string propertyName)
148148
public string GetValue()
149149
{
150150
EnsureBound();
151-
var root = WinappCli.InvokeJson("ui", "get-value", Selector, "-w", Owner!.WindowHandleArg, "--json");
151+
var root = WinappCli.InvokeJson("ui", "get-value", Selector, Owner!.TargetFlag, Owner!.TargetValue, "--json");
152152
if (root.TryGetProperty("text", out var t))
153153
{
154154
return t.GetString() ?? string.Empty;
@@ -166,7 +166,7 @@ public bool WaitForProperty(string propertyName, string expectedValue, int timeo
166166
EnsureBound();
167167
var r = WinappCli.Invoke(
168168
"ui", "wait-for", Selector,
169-
"-w", Owner!.WindowHandleArg,
169+
Owner!.TargetFlag, Owner!.TargetValue,
170170
"--property", propertyName,
171171
"--value", expectedValue,
172172
"-t", timeoutMS.ToString(System.Globalization.CultureInfo.InvariantCulture));
@@ -182,7 +182,7 @@ public bool WaitForGone(int timeoutMS = 5000)
182182
EnsureBound();
183183
var r = WinappCli.Invoke(
184184
"ui", "wait-for", Selector,
185-
"-w", Owner!.WindowHandleArg,
185+
Owner!.TargetFlag, Owner!.TargetValue,
186186
"--gone",
187187
"-t", timeoutMS.ToString(System.Globalization.CultureInfo.InvariantCulture));
188188
return r.ExitCode == 0;

src/common/UITestAutomation.Next/Session.cs

Lines changed: 88 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,47 @@
1010
namespace Microsoft.PowerToys.UITest.Next;
1111

1212
/// <summary>
13-
/// A test session bound to a specific HWND. All <see cref="Find{T}"/>/<see cref="FindAll{T}"/>
14-
/// calls route to <c>winapp ui search</c> with <c>-w &lt;hex hwnd&gt;</c> for stable targeting.
13+
/// A test session bound to either a specific window (HWND) or a whole process (name or PID).
14+
/// All <see cref="Find{T}"/>/<see cref="FindAll{T}"/> calls route to <c>winapp ui search</c>
15+
/// scoped by <see cref="TargetFlag"/>/<see cref="TargetValue"/>.
1516
/// </summary>
17+
/// <remarks>
18+
/// Two scopes are supported:
19+
/// <list type="bullet">
20+
/// <item><description><c>Window</c> (<c>-w &lt;hwnd&gt;</c>) — the default. Use when the
21+
/// process owns multiple windows and the test needs to pin one (e.g. ColorPickerUI's
22+
/// overlay vs editor; Settings vs PopupHost).</description></item>
23+
/// <item><description><c>Process</c> (<c>-a &lt;name|pid&gt;</c>) — simpler when the target
24+
/// process owns exactly one user-facing window. Built via <see cref="FromProcess"/>. Matches
25+
/// the pattern in <see href="https://github.com/microsoft/PowerToys/pull/48414"/>.</description></item>
26+
/// </list>
27+
/// </remarks>
1628
public sealed class Session
1729
{
18-
/// <summary>Decimal HWND of the target window (as returned by <c>list-windows --json</c>).</summary>
30+
public enum TargetScope
31+
{
32+
/// <summary>Scope all CLI calls to a specific HWND via <c>-w</c>.</summary>
33+
Window,
34+
35+
/// <summary>Scope all CLI calls to a process (name substring or PID) via <c>-a</c>.</summary>
36+
Process,
37+
}
38+
39+
/// <summary>Decimal HWND of the target window, or 0 when bound by <see cref="TargetScope.Process"/>.</summary>
1940
public long WindowHandle { get; }
2041

2142
/// <summary>String form of <see cref="WindowHandle"/> for passing to winappcli's <c>-w</c> flag.</summary>
2243
public string WindowHandleArg { get; }
2344

45+
/// <summary>The scope these calls run against (window or process).</summary>
46+
public TargetScope Scope { get; }
47+
48+
/// <summary>winappcli flag for the active scope (<c>-w</c> or <c>-a</c>).</summary>
49+
public string TargetFlag { get; }
50+
51+
/// <summary>Value to pass after <see cref="TargetFlag"/> — the decimal HWND or the process name/PID.</summary>
52+
public string TargetValue { get; }
53+
2454
public string WindowTitle { get; }
2555

2656
public int ProcessId { get; }
@@ -34,11 +64,61 @@ internal Session(PowerToysModule scope, long hwnd, string title, int pid, string
3464
InitScope = scope;
3565
WindowHandle = hwnd;
3666
WindowHandleArg = hwnd.ToString(CultureInfo.InvariantCulture);
67+
Scope = TargetScope.Window;
68+
TargetFlag = "-w";
69+
TargetValue = WindowHandleArg;
70+
WindowTitle = title;
71+
ProcessId = pid;
72+
ProcessName = processName;
73+
}
74+
75+
private Session(PowerToysModule scope, string appNameOrPid, int pid, string processName, string title)
76+
{
77+
InitScope = scope;
78+
WindowHandle = 0;
79+
WindowHandleArg = "0";
80+
Scope = TargetScope.Process;
81+
TargetFlag = "-a";
82+
TargetValue = appNameOrPid;
3783
WindowTitle = title;
3884
ProcessId = pid;
3985
ProcessName = processName;
4086
}
4187

88+
/// <summary>
89+
/// Build a session scoped to a whole process via <c>winapp ... -a &lt;app&gt;</c>. Cheaper than
90+
/// resolving a HWND and ideal for the single-window-per-process case (e.g. Settings smoke
91+
/// tests). The first matching window's PID/name/title are captured for reporting only — all
92+
/// subsequent CLI calls re-resolve via <c>-a</c>, so window-replacement during the test
93+
/// (re-navigation, page swap) is handled transparently.
94+
/// </summary>
95+
/// <param name="appNameOrPid">Process name substring (e.g. <c>"PowerToys.Settings"</c>) or PID as a string.</param>
96+
/// <param name="attributeAs">Module label used for diagnostics only.</param>
97+
/// <param name="timeoutMS">How long to wait for the process to expose at least one UIA window.</param>
98+
public static Session FromProcess(
99+
string appNameOrPid,
100+
PowerToysModule attributeAs = PowerToysModule.Runner,
101+
int timeoutMS = 10_000)
102+
{
103+
var deadline = DateTime.UtcNow + TimeSpan.FromMilliseconds(timeoutMS);
104+
while (DateTime.UtcNow < deadline)
105+
{
106+
var windows = WindowsFinder.ListByApp(appNameOrPid);
107+
if (windows.Count > 0)
108+
{
109+
var w = windows[0];
110+
return new Session(attributeAs, appNameOrPid, w.ProcessId, w.ProcessName, w.Title);
111+
}
112+
113+
Thread.Sleep(250);
114+
}
115+
116+
Assert.Fail(
117+
$"FromProcess('{appNameOrPid}'): no UIA-visible window appeared within {timeoutMS}ms. " +
118+
$"Is the app running? Run 'winapp ui list-windows -a {appNameOrPid}' to confirm.");
119+
return null!;
120+
}
121+
42122
public T Find<T>(By by, int timeoutMS = 5000)
43123
where T : Element, new() => FindUnder<T>(by, timeoutMS);
44124

@@ -136,22 +216,22 @@ public bool WaitFor(Func<bool> condition, int timeoutMS = 5000, int pollInterval
136216
return false;
137217
}
138218

139-
/// <summary>Capture a PNG of the session's window via <c>winapp ui screenshot</c>.</summary>
219+
/// <summary>Capture a PNG of the session's target via <c>winapp ui screenshot</c>.</summary>
140220
public string Screenshot(string outputPath)
141221
{
142-
WinappCli.InvokeAssertSuccess("ui", "screenshot", "-w", WindowHandleArg, "-o", outputPath);
222+
WinappCli.InvokeAssertSuccess("ui", "screenshot", TargetFlag, TargetValue, "-o", outputPath);
143223
return outputPath;
144224
}
145225

146226
/// <summary>
147-
/// Dump the full UIA tree for this session's window via <c>winapp ui inspect --json</c>.
227+
/// Dump the full UIA tree for this session's target via <c>winapp ui inspect --json</c>.
148228
/// Returned shape: <c>{ "windows": [{ "elements": [{ "type", "name", "value", "children": [...] }] }] }</c>.
149229
/// </summary>
150230
public JsonElement Inspect(int depth = 6)
151231
{
152232
return WinappCli.InvokeJson(
153233
"ui", "inspect",
154-
"-w", WindowHandleArg,
234+
TargetFlag, TargetValue,
155235
"--json",
156236
"-d", depth.ToString(CultureInfo.InvariantCulture));
157237
}
@@ -167,7 +247,7 @@ public void Cleanup()
167247
private List<SearchHit> ExecuteSearch(By by)
168248
{
169249
// winappcli accepts the selector text directly as the first positional argument.
170-
var root = WinappCli.InvokeJson("ui", "search", by.Value, "-w", WindowHandleArg, "--json");
250+
var root = WinappCli.InvokeJson("ui", "search", by.Value, TargetFlag, TargetValue, "--json");
171251

172252
var result = new List<SearchHit>();
173253
if (root.TryGetProperty("matches", out var arr) && arr.ValueKind == JsonValueKind.Array)

src/common/UITestAutomation.Next/UITestBase.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ namespace Microsoft.PowerToys.UITest.Next;
2424
[TestClass]
2525
public class UITestBase : IDisposable
2626
{
27+
/// <summary>
28+
/// Lazy one-shot probe for <c>winapp.exe</c>. Runs the first time any UITest in the
29+
/// process initializes — the cost is one extra <c>winapp --version</c> call per test run.
30+
/// </summary>
31+
private static readonly Lazy<bool> CliAvailable = new(WinappCli.IsAvailable);
32+
2733
private readonly PowerToysModule scope;
2834
private SessionHelper? sessionHelper;
2935
private bool disposed;
@@ -40,6 +46,11 @@ protected UITestBase(PowerToysModule scope = PowerToysModule.PowerToysSettings)
4046
[TestInitialize]
4147
public void TestInit()
4248
{
49+
if (!CliAvailable.Value)
50+
{
51+
Assert.Fail(WinappCli.InstallHint);
52+
}
53+
4354
sessionHelper = new SessionHelper(scope);
4455
Session = sessionHelper.Init();
4556
}

0 commit comments

Comments
 (0)