From 0c1be46f608504b0d5f99797bb236a446c0d0496 Mon Sep 17 00:00:00 2001 From: Jelle Buning Date: Tue, 29 Sep 2026 19:28:48 +0200 Subject: [PATCH 1/7] Add WinUia UI automation stack --- .github/README.md | 62 +++- Directory.Build.props | 12 + WinUia.slnx | 20 +- examples/Directory.Build.props | 4 + .../Application/MainForm.cs | 51 ++++ .../Application/MainForm/DialogForm.cs | 12 + .../Application/MainForm/Tab1.cs | 8 + .../Application/MainForm/Tab2.cs | 8 + .../Extensions/AutomationExtensions.cs | 24 ++ .../Tests/DialogTests.cs | 76 +++++ .../Tests/MainFormTests.cs | 165 ++++++++++ .../WinUia.Winforms.Tests/Tests/TabTests.cs | 88 ++++++ .../WinUia.Winforms.Tests.csproj | 9 + .../WinUia.Winforms/DialogForm.Designer.cs | 102 +++++++ examples/WinUia.Winforms/DialogForm.cs | 13 + examples/WinUia.Winforms/MainForm.Designer.cs | 279 +++++++++++++++++ examples/WinUia.Winforms/MainForm.cs | 68 +++++ examples/WinUia.Winforms/MainForm.resx | 120 ++++++++ examples/WinUia.Winforms/Program.cs | 16 + .../WinUia.Winforms/WinUia.Winforms.csproj | 10 + .../WinUia.Winforms.csproj.user | 12 + src/.keep | 0 src/Directory.Build.props | 17 ++ src/WinUia.Core/AutomationContext.cs | 110 +++++++ src/WinUia.Core/ComExceptionMapper.cs | 60 ++++ src/WinUia.Core/ControlType.cs | 51 ++++ src/WinUia.Core/ElementPredicate.cs | 152 ++++++++++ src/WinUia.Core/Elements/Element.Find.cs | 79 +++++ .../Elements/Element.Interaction.cs | 171 +++++++++++ src/WinUia.Core/Elements/Element.cs | 140 +++++++++ src/WinUia.Core/Elements/ElementExtensions.cs | 35 +++ src/WinUia.Core/Elements/ElementIdentity.cs | 25 ++ src/WinUia.Core/Elements/ElementLocator.cs | 57 ++++ .../UiaElementNotEnabledException.cs | 5 + .../Exceptions/UiaElementNotFoundException.cs | 5 + src/WinUia.Core/Exceptions/UiaException.cs | 12 + .../UiaNoClickablePointException.cs | 5 + .../UiaPatternNotSupportedException.cs | 5 + .../Exceptions/UiaStaleElementException.cs | 5 + .../Exceptions/UiaTimeoutException.cs | 5 + src/WinUia.Core/Interop/IUIAutomation.cs | 287 ++++++++++++++++++ .../Patterns/ExpandCollapsePatternWrapper.cs | 21 ++ .../Patterns/ExpandCollapseState.cs | 14 + .../Patterns/InvokePatternWrapper.cs | 14 + src/WinUia.Core/Patterns/PatternWrapper.cs | 41 +++ .../Patterns/ScrollItemPatternWrapper.cs | 14 + .../Patterns/SelectionItemPatternWrapper.cs | 23 ++ .../Patterns/TextPatternWrapper.cs | 15 + .../Patterns/TogglePatternWrapper.cs | 17 ++ src/WinUia.Core/Patterns/ToggleState.cs | 12 + .../Patterns/ValuePatternWrapper.cs | 20 ++ .../Patterns/WindowPatternWrapper.cs | 36 +++ src/WinUia.Core/Patterns/WindowVisualState.cs | 12 + src/WinUia.Core/Retry.cs | 26 ++ src/WinUia.Core/ScreenPoint.cs | 4 + src/WinUia.Core/ScreenRect.cs | 17 ++ src/WinUia.Core/TreeScope.cs | 19 ++ src/WinUia.Core/WinUia.Core.csproj | 12 + src/WinUia.Input/Interop/NativeMethods.cs | 82 +++++ src/WinUia.Input/MouseButton.cs | 10 + src/WinUia.Input/NativeButton.cs | 13 + src/WinUia.Input/PhysicalDpi.cs | 35 +++ src/WinUia.Input/VirtualKey.cs | 31 ++ src/WinUia.Input/Win32InputSimulator.cs | 93 ++++++ src/WinUia.Input/WinUia.Input.csproj | 15 + src/WinUia.NUnit/DesktopLock.cs | 43 +++ src/WinUia.NUnit/UiTestAttribute.cs | 92 ++++++ src/WinUia.NUnit/WinUia.NUnit.csproj | 12 + src/WinUia/AppSession.cs | 61 ++++ src/WinUia/Automation.cs | 147 +++++++++ .../Interop/IApplicationActivationManager.cs | 26 ++ src/WinUia/Launchers/AppLauncher.cs | 122 ++++++++ src/WinUia/Launchers/PackagedAppActivator.cs | 30 ++ src/WinUia/WinUia.csproj | 11 + tests/.keep | 0 tests/Directory.Build.props | 43 +++ .../AutomationContextTests.cs | 109 +++++++ .../AutomationIdValue.cs | 7 + .../ComExceptionMapperTests.cs | 64 ++++ .../ElementPredicateTests.cs | 99 ++++++ .../InteropVtableTests.cs | 69 +++++ tests/WinUia.Core.UnitTests/PatternTests.cs | 229 ++++++++++++++ tests/WinUia.Core.UnitTests/ProcessHolder.cs | 12 + tests/WinUia.Core.UnitTests/RetryTests.cs | 55 ++++ tests/WinUia.Core.UnitTests/TestAppSession.cs | 38 +++ .../WinUia.Core.UnitTests.csproj | 7 + .../NativeLayoutTests.cs | 20 ++ .../PhysicalDpiTests.cs | 40 +++ .../WinUia.Input.UnitTests.csproj | 7 + .../DesktopLockTests.cs | 32 ++ .../UiTestAttributeTests.cs | 54 ++++ .../WinUia.NUnit.UnitTests.csproj | 7 + tests/WinUia.Testing.Shared/TestAppPath.cs | 11 + tests/WinUia.Testing.Shared/UiAssertions.cs | 19 ++ .../WinUia.Testing.Shared.csproj | 22 ++ tests/WinUia.Tests/WinUia.Tests.csproj | 32 ++ tests/WinUia.UnitTests/AppLauncherTests.cs | 123 ++++++++ tests/WinUia.UnitTests/AutomationTests.cs | 69 +++++ .../WinUia.UnitTests/WinUia.UnitTests.csproj | 7 + 99 files changed, 4694 insertions(+), 6 deletions(-) create mode 100644 Directory.Build.props create mode 100644 examples/Directory.Build.props create mode 100644 examples/WinUia.Winforms.Tests/Application/MainForm.cs create mode 100644 examples/WinUia.Winforms.Tests/Application/MainForm/DialogForm.cs create mode 100644 examples/WinUia.Winforms.Tests/Application/MainForm/Tab1.cs create mode 100644 examples/WinUia.Winforms.Tests/Application/MainForm/Tab2.cs create mode 100644 examples/WinUia.Winforms.Tests/Extensions/AutomationExtensions.cs create mode 100644 examples/WinUia.Winforms.Tests/Tests/DialogTests.cs create mode 100644 examples/WinUia.Winforms.Tests/Tests/MainFormTests.cs create mode 100644 examples/WinUia.Winforms.Tests/Tests/TabTests.cs create mode 100644 examples/WinUia.Winforms.Tests/WinUia.Winforms.Tests.csproj create mode 100644 examples/WinUia.Winforms/DialogForm.Designer.cs create mode 100644 examples/WinUia.Winforms/DialogForm.cs create mode 100644 examples/WinUia.Winforms/MainForm.Designer.cs create mode 100644 examples/WinUia.Winforms/MainForm.cs create mode 100644 examples/WinUia.Winforms/MainForm.resx create mode 100644 examples/WinUia.Winforms/Program.cs create mode 100644 examples/WinUia.Winforms/WinUia.Winforms.csproj create mode 100644 examples/WinUia.Winforms/WinUia.Winforms.csproj.user delete mode 100644 src/.keep create mode 100644 src/Directory.Build.props create mode 100644 src/WinUia.Core/AutomationContext.cs create mode 100644 src/WinUia.Core/ComExceptionMapper.cs create mode 100644 src/WinUia.Core/ControlType.cs create mode 100644 src/WinUia.Core/ElementPredicate.cs create mode 100644 src/WinUia.Core/Elements/Element.Find.cs create mode 100644 src/WinUia.Core/Elements/Element.Interaction.cs create mode 100644 src/WinUia.Core/Elements/Element.cs create mode 100644 src/WinUia.Core/Elements/ElementExtensions.cs create mode 100644 src/WinUia.Core/Elements/ElementIdentity.cs create mode 100644 src/WinUia.Core/Elements/ElementLocator.cs create mode 100644 src/WinUia.Core/Exceptions/UiaElementNotEnabledException.cs create mode 100644 src/WinUia.Core/Exceptions/UiaElementNotFoundException.cs create mode 100644 src/WinUia.Core/Exceptions/UiaException.cs create mode 100644 src/WinUia.Core/Exceptions/UiaNoClickablePointException.cs create mode 100644 src/WinUia.Core/Exceptions/UiaPatternNotSupportedException.cs create mode 100644 src/WinUia.Core/Exceptions/UiaStaleElementException.cs create mode 100644 src/WinUia.Core/Exceptions/UiaTimeoutException.cs create mode 100644 src/WinUia.Core/Interop/IUIAutomation.cs create mode 100644 src/WinUia.Core/Patterns/ExpandCollapsePatternWrapper.cs create mode 100644 src/WinUia.Core/Patterns/ExpandCollapseState.cs create mode 100644 src/WinUia.Core/Patterns/InvokePatternWrapper.cs create mode 100644 src/WinUia.Core/Patterns/PatternWrapper.cs create mode 100644 src/WinUia.Core/Patterns/ScrollItemPatternWrapper.cs create mode 100644 src/WinUia.Core/Patterns/SelectionItemPatternWrapper.cs create mode 100644 src/WinUia.Core/Patterns/TextPatternWrapper.cs create mode 100644 src/WinUia.Core/Patterns/TogglePatternWrapper.cs create mode 100644 src/WinUia.Core/Patterns/ToggleState.cs create mode 100644 src/WinUia.Core/Patterns/ValuePatternWrapper.cs create mode 100644 src/WinUia.Core/Patterns/WindowPatternWrapper.cs create mode 100644 src/WinUia.Core/Patterns/WindowVisualState.cs create mode 100644 src/WinUia.Core/Retry.cs create mode 100644 src/WinUia.Core/ScreenPoint.cs create mode 100644 src/WinUia.Core/ScreenRect.cs create mode 100644 src/WinUia.Core/TreeScope.cs create mode 100644 src/WinUia.Core/WinUia.Core.csproj create mode 100644 src/WinUia.Input/Interop/NativeMethods.cs create mode 100644 src/WinUia.Input/MouseButton.cs create mode 100644 src/WinUia.Input/NativeButton.cs create mode 100644 src/WinUia.Input/PhysicalDpi.cs create mode 100644 src/WinUia.Input/VirtualKey.cs create mode 100644 src/WinUia.Input/Win32InputSimulator.cs create mode 100644 src/WinUia.Input/WinUia.Input.csproj create mode 100644 src/WinUia.NUnit/DesktopLock.cs create mode 100644 src/WinUia.NUnit/UiTestAttribute.cs create mode 100644 src/WinUia.NUnit/WinUia.NUnit.csproj create mode 100644 src/WinUia/AppSession.cs create mode 100644 src/WinUia/Automation.cs create mode 100644 src/WinUia/Interop/IApplicationActivationManager.cs create mode 100644 src/WinUia/Launchers/AppLauncher.cs create mode 100644 src/WinUia/Launchers/PackagedAppActivator.cs create mode 100644 src/WinUia/WinUia.csproj delete mode 100644 tests/.keep create mode 100644 tests/Directory.Build.props create mode 100644 tests/WinUia.Core.UnitTests/AutomationContextTests.cs create mode 100644 tests/WinUia.Core.UnitTests/AutomationIdValue.cs create mode 100644 tests/WinUia.Core.UnitTests/ComExceptionMapperTests.cs create mode 100644 tests/WinUia.Core.UnitTests/ElementPredicateTests.cs create mode 100644 tests/WinUia.Core.UnitTests/InteropVtableTests.cs create mode 100644 tests/WinUia.Core.UnitTests/PatternTests.cs create mode 100644 tests/WinUia.Core.UnitTests/ProcessHolder.cs create mode 100644 tests/WinUia.Core.UnitTests/RetryTests.cs create mode 100644 tests/WinUia.Core.UnitTests/TestAppSession.cs create mode 100644 tests/WinUia.Core.UnitTests/WinUia.Core.UnitTests.csproj create mode 100644 tests/WinUia.Input.UnitTests/NativeLayoutTests.cs create mode 100644 tests/WinUia.Input.UnitTests/PhysicalDpiTests.cs create mode 100644 tests/WinUia.Input.UnitTests/WinUia.Input.UnitTests.csproj create mode 100644 tests/WinUia.NUnit.UnitTests/DesktopLockTests.cs create mode 100644 tests/WinUia.NUnit.UnitTests/UiTestAttributeTests.cs create mode 100644 tests/WinUia.NUnit.UnitTests/WinUia.NUnit.UnitTests.csproj create mode 100644 tests/WinUia.Testing.Shared/TestAppPath.cs create mode 100644 tests/WinUia.Testing.Shared/UiAssertions.cs create mode 100644 tests/WinUia.Testing.Shared/WinUia.Testing.Shared.csproj create mode 100644 tests/WinUia.Tests/WinUia.Tests.csproj create mode 100644 tests/WinUia.UnitTests/AppLauncherTests.cs create mode 100644 tests/WinUia.UnitTests/AutomationTests.cs create mode 100644 tests/WinUia.UnitTests/WinUia.UnitTests.csproj diff --git a/.github/README.md b/.github/README.md index 3cbed40..884b783 100644 --- a/.github/README.md +++ b/.github/README.md @@ -34,6 +34,12 @@
  • Installation
  • +
  • + Usage + +
  • Contributing
  • License
  • @@ -47,14 +53,16 @@ WinUia is a .NET library for inspecting and automating Windows applications thro ### Features -* **Feature One:** Describe a core responsibility or benefit here. -* **Feature Two:** Highlight another modular component or capability. -* **Modern Architecture:** Built with SOLID principles and design patterns for maximum maintainability. +* **No dependencies:** talks to the native UIA3 COM API (`UIAutomationCore.dll`) through hand-written interop. No FlaUI, no interop packages. +* **Works across UI stacks:** WinForms, WPF, WinUI 3 (packaged and unpackaged), UWP and classic Win32 apps, x86 or x64. +* **Self-healing elements:** an element remembers how it was found and re-finds itself when a re-render makes it stale. +* **Waiting built in:** `Find` polls until the element appears (default 5 s), `TryFind` returns `null` instead of throwing. +* **Patterns first, mouse last:** `Click()` and `SetValue()` use UIA patterns (Invoke, Toggle, SelectionItem, ExpandCollapse, Value) and only fall back to `SendInput`. ### Built With -* [Framework/Language Name](https://example.com) -* [Library Name](https://example.com) +* [.NET 10](https://dotnet.microsoft.com/) +* [Microsoft UI Automation](https://learn.microsoft.com/windows/win32/winauto/entry-uiauto-win32) ## Getting Started Setting up WinUia on your local machine is straightforward. Make sure the [.NET 10 SDK](https://dotnet.microsoft.com/download) is installed. @@ -80,6 +88,50 @@ Setting up WinUia on your local machine is straightforward. Make sure the [.NET dotnet test WinUia.slnx ``` + The UI tests (`Category=UI`) launch the bundled `WinUia.Winforms` and need an interactive desktop. Run only the others with `dotnet test WinUia.slnx --filter "Category!=UI"`. + +## Usage + +```csharp +using WinUia; + +using var app = Automation.Launch(@"C:\path\to\MyApp.exe"); // or Automation.LaunchPackaged("Publisher.App_hash!App"), Automation.Attach(pid) + +app.Find("txtName").SetValue("Ada"); // by AutomationId, then by Name +app.Find("btnSave").Click(); + +var status = app.MainWindow.FindByAutomationId("lblStatus", TimeSpan.FromSeconds(10)); +Console.WriteLine(status.Name); +// Disposing a launched app closes it (Window pattern first, then kill). +``` + +### Project structure + +WinUia is a modular monolith: one solution, one project per module, and one test project per module. Dependencies point one way, from the application layer down to the platform: + +| Module | Layer | Contents | Depends on | +|---|---|---|---| +| `WinUia` | Application | `Automation` (page objects over a running app), `WinUia.Launchers` (`AppLauncher`, packaged-app activation) | `WinUia.Core` (public API only) | +| `WinUia.Core` | Domain | `AutomationContext`, `WinUia.Core.Elements` (`Element`, lambda searches such as `Find(e => e.Name == "OK")`), `WinUia.Core.Patterns`, `WinUia.Core.Exceptions`, the UIA COM interop | `WinUia.Input` | +| `WinUia.Input` | Platform | `Win32InputSimulator` (`SendInput`), DPI scope, native button clicks | — | +| `WinUia.NUnit` | Test integration | `[UiTest]`: one desktop per test, across test processes | NUnit | + +Module rules: + +* Each module's `Interop/` folder is private to that module. Other modules use what it offers (`PhysicalDpi`, `NativeButton`), never its P/Invoke or COM declarations. +* `WinUia` uses Core's public API only, the same API an application built on WinUia gets. +* Internals are shared only through `InternalsVisibleTo`, and only with the module's own `.UnitTests` project, plus `WinUia.Input` with `WinUia.Core`. +* One type per file. + +Tests live next to their module as `tests/.UnitTests` and share `examples/WinUia.Winforms`, a WinForms fixture app (tested itself by `examples/WinUia.Winforms.Tests`), located through `tests/WinUia.Testing.Shared`. Tests use NUnit. + +### Limitations + +* **MTA only:** UI Automation must be called from an MTA thread. Creating an `AutomationContext` or `Automation` on an STA thread (for example a WinForms/WPF UI thread, or an `[STAThread]` `Main`) throws. Use `Task.Run` or a thread-pool thread. +* **Physical input needs a real desktop:** the mouse and keyboard fallbacks use `SendInput`, which does nothing on a locked workstation or a disconnected RDP session, and goes to whatever window is on top. +* **Elevation (UIPI):** a non-elevated process cannot send input to, and has limited UIA access to, an elevated app. Run the automation elevated when the target is. +* **DPI:** creating an `AutomationContext` makes the process per-monitor DPI aware (if it is not already) so UIA coordinates and `SendInput` both use physical pixels. + ## Contributing diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..8461047 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,12 @@ + + + net10.0-windows + latest + enable + enable + true + true + + true + + diff --git a/WinUia.slnx b/WinUia.slnx index 4e2253d..b5192ef 100644 --- a/WinUia.slnx +++ b/WinUia.slnx @@ -1 +1,19 @@ - + + + + + + + + + + + + + + + + + + + diff --git a/examples/Directory.Build.props b/examples/Directory.Build.props new file mode 100644 index 0000000..e12c031 --- /dev/null +++ b/examples/Directory.Build.props @@ -0,0 +1,4 @@ + + + + diff --git a/examples/WinUia.Winforms.Tests/Application/MainForm.cs b/examples/WinUia.Winforms.Tests/Application/MainForm.cs new file mode 100644 index 0000000..e40cae7 --- /dev/null +++ b/examples/WinUia.Winforms.Tests/Application/MainForm.cs @@ -0,0 +1,51 @@ +using System.Linq.Expressions; +using WinUia.Core; +using WinUia.Core.Elements; + +namespace WinUia.Winforms.Tests.Application; + +public class MainForm : Automation +{ + public Element Window => MainWindow; + public Element Button => Window.FindByAutomationId("btnClick"); + public Element ResultLabel => Window.FindByAutomationId("lblResult"); + public Element InputBox => Window.FindByAutomationId("txtInput"); + public Element ToggleCheckBox => Window.FindByAutomationId("chkToggle"); + public Element ItemsList => Window.FindByAutomationId("lstItems"); + public Element RecreateButton => Window.FindByAutomationId("btnRecreate"); + public Element VolatileButton => Window.FindByAutomationId("btnVolatile"); + public Element ItemsPanel => Window.FindByAutomationId("pnlItems"); + public Element ReverseButton => Window.FindByAutomationId("btnReverse"); + public Element OpenDialogButton => Window.FindByAutomationId("btnOpenDialog"); + public Element TabControl => Window.FindByAutomationId("tabMain"); + + public DialogForm OpenDialog() + { + OpenDialogButton.Click(); + return new DialogForm(FindDialog("WinUia Dialog")); + } + + // A modal dialog is a top-level window owned by the main window: UIA lists it under its owner or under the desktop. + private Element FindDialog(string title) + { + var processId = Process.Id; + Expression> isDialog = e => e.Name == title && e.ControlType == ControlType.Window && e.ProcessId == processId; + var desktop = Context.GetRootElement(); + return Context.WaitFor(() => Window.TryFind(isDialog, TreeScope.Children, TimeSpan.Zero) + ?? desktop.TryFind(isDialog, TreeScope.Children, TimeSpan.Zero)) + ?? throw new InvalidOperationException($"The dialog '{title}' did not appear."); + } + + // The tab header (TabItem) and its page (Pane) share the tab's text, so ask for the TabItem explicitly. + public Element TabHeader(string text) => + TabControl.Find(e => e.Name == text && e.ControlType == ControlType.TabItem, TreeScope.Children); + + public Tab1 SelectTab1() => SelectTab("Tab 1"); + public Tab2 SelectTab2() => SelectTab("Tab 2"); + + private TTab SelectTab(string header) where TTab : MainForm, new() + { + TabHeader(header).Select(); + return As(); + } +} diff --git a/examples/WinUia.Winforms.Tests/Application/MainForm/DialogForm.cs b/examples/WinUia.Winforms.Tests/Application/MainForm/DialogForm.cs new file mode 100644 index 0000000..8447518 --- /dev/null +++ b/examples/WinUia.Winforms.Tests/Application/MainForm/DialogForm.cs @@ -0,0 +1,12 @@ +using WinUia.Core.Elements; + +namespace WinUia.Winforms.Tests.Application; + +public sealed class DialogForm(Element window) +{ + public Element Window { get; } = window; + public Element MessageLabel => Window.FindByAutomationId("lblDialogMessage"); + public Element InputBox => Window.FindByAutomationId("txtDialogInput"); + public Element OkButton => Window.FindByAutomationId("btnDialogOk"); + public Element CancelButton => Window.FindByAutomationId("btnDialogCancel"); +} diff --git a/examples/WinUia.Winforms.Tests/Application/MainForm/Tab1.cs b/examples/WinUia.Winforms.Tests/Application/MainForm/Tab1.cs new file mode 100644 index 0000000..9ba3de1 --- /dev/null +++ b/examples/WinUia.Winforms.Tests/Application/MainForm/Tab1.cs @@ -0,0 +1,8 @@ +using WinUia.Core.Elements; + +namespace WinUia.Winforms.Tests.Application; + +public sealed class Tab1 : MainForm +{ + public Element Content => Window.FindByAutomationId("lblTab1"); +} diff --git a/examples/WinUia.Winforms.Tests/Application/MainForm/Tab2.cs b/examples/WinUia.Winforms.Tests/Application/MainForm/Tab2.cs new file mode 100644 index 0000000..b192c6b --- /dev/null +++ b/examples/WinUia.Winforms.Tests/Application/MainForm/Tab2.cs @@ -0,0 +1,8 @@ +using WinUia.Core.Elements; + +namespace WinUia.Winforms.Tests.Application; + +public sealed class Tab2 : MainForm +{ + public Element Content => Window.FindByAutomationId("lblTab2"); +} diff --git a/examples/WinUia.Winforms.Tests/Extensions/AutomationExtensions.cs b/examples/WinUia.Winforms.Tests/Extensions/AutomationExtensions.cs new file mode 100644 index 0000000..0cfe0de --- /dev/null +++ b/examples/WinUia.Winforms.Tests/Extensions/AutomationExtensions.cs @@ -0,0 +1,24 @@ +using WinUia.Winforms.Tests.Application; + +namespace WinUia.Winforms.Tests.Extensions; + +public static class AutomationExtensions +{ + extension(Automation) + { + public static MainForm LaunchApplication(string path, string? arguments = null) + { + var form = Automation.Launch(path, arguments); + try + { + _ = form.MainWindow; // Fail here, not on first use, when the window never appears. + return form; + } + catch + { + form.Dispose(); + throw; + } + } + } +} diff --git a/examples/WinUia.Winforms.Tests/Tests/DialogTests.cs b/examples/WinUia.Winforms.Tests/Tests/DialogTests.cs new file mode 100644 index 0000000..ad6e460 --- /dev/null +++ b/examples/WinUia.Winforms.Tests/Tests/DialogTests.cs @@ -0,0 +1,76 @@ +using System.Diagnostics; +using WinUia.Core.Exceptions; +using WinUia.Testing.Shared; +using WinUia.Winforms.Tests.Application; +using WinUia.Winforms.Tests.Extensions; + +namespace WinUia.Winforms.Tests.Tests; + +[UiTest] +public sealed class DialogTests +{ + private MainForm _mainForm = null!; + + [SetUp] + public void SetUp() => _mainForm = Automation.LaunchApplication(TestAppPath.Exe); + + [Test] + public void Opening_the_dialog_shows_a_modal_window() + { + var stopwatch = Stopwatch.StartNew(); + var dialog = _mainForm.OpenDialog(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(stopwatch.Elapsed, Is.LessThan(TimeSpan.FromSeconds(5)), "Click must not block while the modal dialog is open"); + Assert.That(dialog.Window.Name, Is.EqualTo("WinUia Dialog")); + Assert.That(dialog.Window.WindowPattern.IsModal, Is.True); + Assert.That(dialog.MessageLabel.Name, Is.EqualTo("Enter a value:")); + } + } + + [Test] + public void Confirming_the_dialog_reports_the_entered_value() + { + var dialog = _mainForm.OpenDialog(); + + dialog.InputBox.SetValue("hello"); + dialog.OkButton.Click(); + + Eventually(() => _mainForm.ResultLabel.Name == "Dialog: OK (hello)"); + } + + [Test] + public void Cancelling_the_dialog_reports_cancel() + { + var dialog = _mainForm.OpenDialog(); + + dialog.CancelButton.Click(); + + Eventually(() => _mainForm.ResultLabel.Name == "Dialog: Cancel"); + } + + [Test] + public void Closing_the_dialog_removes_it() + { + var dialog = _mainForm.OpenDialog(); + + dialog.CancelButton.Click(); + + Eventually(() => + { + try + { + _ = dialog.Window.Name; + return false; + } + catch (UiaStaleElementException) + { + return true; // Closed: the window is gone and cannot be found again. + } + }); + } + + [TearDown] + public void TearDown() => _mainForm.Dispose(); +} diff --git a/examples/WinUia.Winforms.Tests/Tests/MainFormTests.cs b/examples/WinUia.Winforms.Tests/Tests/MainFormTests.cs new file mode 100644 index 0000000..3e22951 --- /dev/null +++ b/examples/WinUia.Winforms.Tests/Tests/MainFormTests.cs @@ -0,0 +1,165 @@ +using WinUia.Core; +using WinUia.Core.Exceptions; +using WinUia.Core.Patterns; +using WinUia.Testing.Shared; +using WinUia.Winforms.Tests.Application; +using WinUia.Winforms.Tests.Extensions; + +namespace WinUia.Winforms.Tests.Tests; + +[UiTest] +public sealed class MainFormTests +{ + private MainForm _mainForm = null!; + + [SetUp] + public void SetUp() => _mainForm = Automation.LaunchApplication(TestAppPath.Exe); + + [Test] + public void Launch_returns_the_main_form() + { + using (Assert.EnterMultipleScope()) + { + Assert.That(_mainForm.Window.Name, Is.EqualTo("WinUia Test App")); + Assert.That(_mainForm.Window.ProcessId, Is.EqualTo(_mainForm.Process.Id)); + } + } + + [Test] + public void The_form_starts_in_its_initial_state() + { + using (Assert.EnterMultipleScope()) + { + Assert.That(_mainForm.ResultLabel.Name, Is.EqualTo("Ready")); + Assert.That(_mainForm.InputBox.ValuePattern.Value, Is.Empty); + Assert.That(_mainForm.ToggleCheckBox.TogglePattern.State, Is.EqualTo(ToggleState.Off)); + Assert.That(_mainForm.VolatileButton.Name, Is.EqualTo("Volatile 1")); + Assert.That(ItemNames(), Is.EqualTo(["Item A", "Item B", "Item C"])); + Assert.That(_mainForm.ItemsList.FindAll(e => e.ControlType == ControlType.ListItem, TreeScope.Children).Select(i => i.Name), Is.EqualTo(["Alpha", "Beta", "Gamma"])); + } + } + + [Test] + public void Selecting_a_list_item_selects_it() + { + var beta = _mainForm.ItemsList.Find(e => e.Name == "Beta"); + + beta.Select(); + + Assert.That(beta.SelectionItemPattern.IsSelected, Is.True); + } + + [Test] + public void Recreate_increments_the_volatile_button_generation() + { + _mainForm.RecreateButton.Click(); + Eventually(() => _mainForm.VolatileButton.Name == "Volatile 2"); + + _mainForm.RecreateButton.Click(); + Eventually(() => _mainForm.VolatileButton.Name == "Volatile 3"); + } + + [Test] + public void Reverse_twice_restores_the_item_order() + { + _mainForm.ReverseButton.Click(); + Eventually(() => ItemNames().SequenceEqual(["Item C", "Item B", "Item A"])); + + _mainForm.ReverseButton.Click(); + Eventually(() => ItemNames().SequenceEqual(["Item A", "Item B", "Item C"])); + } + + [Test] + public void Clicking_the_button_updates_the_label() + { + _mainForm.Button.Click(); + + Eventually(() => _mainForm.ResultLabel.Name == "Clicked"); + } + + [Test] + public void SetValue_round_trips() + { + var input = _mainForm.InputBox; + + input.SetValue("round trip"); + + Assert.That(input.ValuePattern.Value, Is.EqualTo("round trip")); + } + + [Test] + public void Toggle_changes_the_checkbox() + { + var toggle = _mainForm.ToggleCheckBox; + + toggle.Toggle(); + + Assert.That(toggle.TogglePattern.State, Is.EqualTo(ToggleState.On)); + } + + [Test] + public void A_found_element_re_resolves_after_its_control_is_recreated() + { + var volatileButton = _mainForm.VolatileButton; + Assert.That(volatileButton.Name, Is.EqualTo("Volatile 1")); + + _mainForm.RecreateButton.Click(); + Eventually(() => _mainForm.VolatileButton.Name == "Volatile 2"); + + Assert.That(volatileButton.Name, Is.EqualTo("Volatile 2")); + } + + [Test] + public void FindAll_elements_re_resolve_to_the_same_control_when_siblings_move() + { + var items = _mainForm.ItemsPanel.FindAll(e => e.ControlType == ControlType.Button, TreeScope.Children); + var namesBefore = items.Select(i => i.Name).ToArray(); + Assert.That(namesBefore, Is.EquivalentTo(["Item A", "Item B", "Item C"])); + + _mainForm.ReverseButton.Click(); + Eventually(() => ItemNames().SequenceEqual(namesBefore.Reverse())); + + Assert.That(items.Select(i => i.Name), Is.EqualTo(namesBefore)); + } + + [Test] + public void A_FindAll_element_never_read_before_going_stale_reports_stale_instead_of_guessing() + { + var items = _mainForm.ItemsPanel.FindAll(e => e.ControlType == ControlType.Button, TreeScope.Children); + + _mainForm.ReverseButton.Click(); + Eventually(() => ItemNames().First() == "Item C"); + + Assert.Throws(() => _ = items[0].Name); + } + + [Test] + public void Without_stale_retries_a_recreated_control_reports_stale() + { + var volatileButton = _mainForm.VolatileButton; + Assert.That(volatileButton.Name, Is.EqualTo("Volatile 1")); + _mainForm.RecreateButton.Click(); + Eventually(() => _mainForm.VolatileButton.Name == "Volatile 2"); + + _mainForm.Context.StaleRetryCount = 0; + + Assert.Throws(() => _ = volatileButton.Name); + } + + [Test] + public void An_element_without_a_locator_reports_stale_after_its_control_is_recreated() + { + var byHandle = _mainForm.Context.FromHandle(_mainForm.VolatileButton.NativeWindowHandle); + Assert.That(byHandle.Name, Is.EqualTo("Volatile 1")); + + _mainForm.RecreateButton.Click(); + Eventually(() => _mainForm.VolatileButton.Name == "Volatile 2"); + + Assert.Throws(() => _ = byHandle.Name); + } + + [TearDown] + public void TearDown() => _mainForm.Dispose(); + + private IEnumerable ItemNames() => _mainForm.ItemsPanel.FindAll(e => e.ControlType == ControlType.Button, TreeScope.Children).Select(i => i.Name); +} diff --git a/examples/WinUia.Winforms.Tests/Tests/TabTests.cs b/examples/WinUia.Winforms.Tests/Tests/TabTests.cs new file mode 100644 index 0000000..b36b460 --- /dev/null +++ b/examples/WinUia.Winforms.Tests/Tests/TabTests.cs @@ -0,0 +1,88 @@ +using WinUia.Testing.Shared; +using WinUia.Winforms.Tests.Application; +using WinUia.Winforms.Tests.Extensions; + +namespace WinUia.Winforms.Tests.Tests; + +[UiTest] +public sealed class TabTests +{ + private MainForm _mainForm = null!; + + [SetUp] + public void SetUp() => _mainForm = Automation.LaunchApplication(TestAppPath.Exe); + + [Test] + public void Tab_1_is_selected_at_start() + { + using (Assert.EnterMultipleScope()) + { + Assert.That(_mainForm.TabHeader("Tab 1").SelectionItemPattern.IsSelected, Is.True); + Assert.That(_mainForm.TabHeader("Tab 2").SelectionItemPattern.IsSelected, Is.False); + } + } + + [Test] + public void SelectTab1_shows_the_tab_1_content() + { + var tab1 = _mainForm.SelectTab1(); + + Assert.That(tab1.Content.Name, Is.EqualTo("tab1 content")); + } + + [Test] + public void SelectTab2_selects_the_tab_and_shows_the_tab_2_content() + { + var tab2 = _mainForm.SelectTab2(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(_mainForm.TabHeader("Tab 2").SelectionItemPattern.IsSelected, Is.True); + Assert.That(tab2.Content.Name, Is.EqualTo("tab2 content")); + } + } + + [Test] + public void Switching_tabs_hides_the_other_tabs_content() + { + _mainForm.SelectTab2(); + var tab1 = _mainForm.SelectTab1(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(tab1.Content.Name, Is.EqualTo("tab1 content")); + Assert.That(tab1.Window.TryFindByAutomationId("lblTab2", TimeSpan.FromMilliseconds(200)), Is.Null); + } + } + + [Test] + public void A_tab_is_the_same_application_and_keeps_the_main_form_controls() + { + var tab2 = _mainForm.SelectTab2(); + + tab2.Button.Click(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(tab2.Process.Id, Is.EqualTo(_mainForm.Process.Id)); + Eventually(() => _mainForm.ResultLabel.Name == "Clicked"); + } + } + + [Test] + public void Disposing_a_tab_leaves_the_application_running() + { + using (_mainForm.SelectTab2()) + { + } + + using (Assert.EnterMultipleScope()) + { + Assert.That(_mainForm.Process.HasExited, Is.False); + Assert.That(_mainForm.SelectTab1().Content.Name, Is.EqualTo("tab1 content")); + } + } + + [TearDown] + public void TearDown() => _mainForm.Dispose(); +} diff --git a/examples/WinUia.Winforms.Tests/WinUia.Winforms.Tests.csproj b/examples/WinUia.Winforms.Tests/WinUia.Winforms.Tests.csproj new file mode 100644 index 0000000..a8f6a27 --- /dev/null +++ b/examples/WinUia.Winforms.Tests/WinUia.Winforms.Tests.csproj @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/examples/WinUia.Winforms/DialogForm.Designer.cs b/examples/WinUia.Winforms/DialogForm.Designer.cs new file mode 100644 index 0000000..4f998bf --- /dev/null +++ b/examples/WinUia.Winforms/DialogForm.Designer.cs @@ -0,0 +1,102 @@ +namespace WinUia.Winforms +{ + partial class DialogForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + lblDialogMessage = new Label(); + txtDialogInput = new TextBox(); + btnDialogOk = new Button(); + btnDialogCancel = new Button(); + SuspendLayout(); + // + // lblDialogMessage + // + lblDialogMessage.AutoSize = true; + lblDialogMessage.Location = new Point(12, 12); + lblDialogMessage.Name = "lblDialogMessage"; + lblDialogMessage.Size = new Size(84, 15); + lblDialogMessage.TabIndex = 0; + lblDialogMessage.Text = "Enter a value:"; + // + // txtDialogInput + // + txtDialogInput.Location = new Point(12, 35); + txtDialogInput.Name = "txtDialogInput"; + txtDialogInput.Size = new Size(236, 23); + txtDialogInput.TabIndex = 1; + // + // btnDialogOk + // + btnDialogOk.DialogResult = DialogResult.OK; + btnDialogOk.Location = new Point(92, 70); + btnDialogOk.Name = "btnDialogOk"; + btnDialogOk.Size = new Size(75, 23); + btnDialogOk.TabIndex = 2; + btnDialogOk.Text = "OK"; + btnDialogOk.UseVisualStyleBackColor = true; + // + // btnDialogCancel + // + btnDialogCancel.DialogResult = DialogResult.Cancel; + btnDialogCancel.Location = new Point(173, 70); + btnDialogCancel.Name = "btnDialogCancel"; + btnDialogCancel.Size = new Size(75, 23); + btnDialogCancel.TabIndex = 3; + btnDialogCancel.Text = "Cancel"; + btnDialogCancel.UseVisualStyleBackColor = true; + // + // DialogForm + // + AcceptButton = btnDialogOk; + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + CancelButton = btnDialogCancel; + ClientSize = new Size(260, 105); + Controls.Add(lblDialogMessage); + Controls.Add(txtDialogInput); + Controls.Add(btnDialogOk); + Controls.Add(btnDialogCancel); + FormBorderStyle = FormBorderStyle.FixedDialog; + MaximizeBox = false; + MinimizeBox = false; + Name = "DialogForm"; + ShowInTaskbar = false; + StartPosition = FormStartPosition.CenterParent; + Text = "WinUia Dialog"; + ResumeLayout(false); + PerformLayout(); + } + + #endregion + + private Label lblDialogMessage; + private TextBox txtDialogInput; + private Button btnDialogOk; + private Button btnDialogCancel; + } +} diff --git a/examples/WinUia.Winforms/DialogForm.cs b/examples/WinUia.Winforms/DialogForm.cs new file mode 100644 index 0000000..bc92a73 --- /dev/null +++ b/examples/WinUia.Winforms/DialogForm.cs @@ -0,0 +1,13 @@ +namespace WinUia.Winforms; + +/// Modal dialog opened by btnOpenDialog: asks for a value, closed with OK or Cancel. +public partial class DialogForm : Form +{ + public DialogForm() + { + InitializeComponent(); + } + + /// The value typed into the dialog. + public string InputText => txtDialogInput.Text; +} diff --git a/examples/WinUia.Winforms/MainForm.Designer.cs b/examples/WinUia.Winforms/MainForm.Designer.cs new file mode 100644 index 0000000..ba070d1 --- /dev/null +++ b/examples/WinUia.Winforms/MainForm.Designer.cs @@ -0,0 +1,279 @@ +namespace WinUia.Winforms +{ + partial class MainForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + lblResult = new Label(); + btnClick = new Button(); + txtInput = new TextBox(); + chkToggle = new CheckBox(); + lstItems = new ListBox(); + btnRecreate = new Button(); + btnVolatile = new Button(); + pnlItems = new FlowLayoutPanel(); + btnItemA = new Button(); + btnItemB = new Button(); + btnItemC = new Button(); + btnReverse = new Button(); + btnOpenDialog = new Button(); + tabMain = new TabControl(); + tabPage1 = new TabPage(); + lblTab1 = new Label(); + tabPage2 = new TabPage(); + lblTab2 = new Label(); + pnlItems.SuspendLayout(); + tabMain.SuspendLayout(); + tabPage1.SuspendLayout(); + tabPage2.SuspendLayout(); + SuspendLayout(); + // + // lblResult + // + lblResult.AutoSize = true; + lblResult.Location = new Point(12, 12); + lblResult.Name = "lblResult"; + lblResult.Size = new Size(39, 15); + lblResult.TabIndex = 0; + lblResult.Text = "Ready"; + // + // btnClick + // + btnClick.Location = new Point(12, 40); + btnClick.Name = "btnClick"; + btnClick.Size = new Size(120, 23); + btnClick.TabIndex = 1; + btnClick.Text = "Click me"; + btnClick.UseVisualStyleBackColor = true; + btnClick.Click += btnClick_Click; + // + // txtInput + // + txtInput.Location = new Point(12, 75); + txtInput.Name = "txtInput"; + txtInput.Size = new Size(200, 23); + txtInput.TabIndex = 2; + // + // chkToggle + // + chkToggle.AutoSize = true; + chkToggle.Location = new Point(12, 105); + chkToggle.Name = "chkToggle"; + chkToggle.Size = new Size(80, 19); + chkToggle.TabIndex = 3; + chkToggle.Text = "Toggle me"; + chkToggle.UseVisualStyleBackColor = true; + // + // lstItems + // + lstItems.FormattingEnabled = true; + lstItems.ItemHeight = 15; + lstItems.Items.AddRange(new object[] { "Alpha", "Beta", "Gamma" }); + lstItems.Location = new Point(230, 40); + lstItems.Name = "lstItems"; + lstItems.SelectionMode = SelectionMode.MultiExtended; + lstItems.Size = new Size(110, 79); + lstItems.TabIndex = 4; + // + // btnRecreate + // + btnRecreate.Location = new Point(12, 140); + btnRecreate.Name = "btnRecreate"; + btnRecreate.Size = new Size(120, 23); + btnRecreate.TabIndex = 5; + btnRecreate.Text = "Recreate"; + btnRecreate.UseVisualStyleBackColor = true; + btnRecreate.Click += btnRecreate_Click; + // + // btnVolatile + // + btnVolatile.Location = new Point(140, 140); + btnVolatile.Name = "btnVolatile"; + btnVolatile.Size = new Size(120, 23); + btnVolatile.TabIndex = 6; + btnVolatile.Text = "Volatile 1"; + btnVolatile.UseVisualStyleBackColor = true; + // + // pnlItems + // + pnlItems.Controls.Add(btnItemA); + pnlItems.Controls.Add(btnItemB); + pnlItems.Controls.Add(btnItemC); + pnlItems.Location = new Point(12, 175); + pnlItems.Name = "pnlItems"; + pnlItems.Size = new Size(330, 35); + pnlItems.TabIndex = 7; + // + // btnItemA + // + btnItemA.Location = new Point(3, 3); + btnItemA.Name = "btnItemA"; + btnItemA.Size = new Size(100, 23); + btnItemA.TabIndex = 0; + btnItemA.Text = "Item A"; + btnItemA.UseVisualStyleBackColor = true; + // + // btnItemB + // + btnItemB.Location = new Point(109, 3); + btnItemB.Name = "btnItemB"; + btnItemB.Size = new Size(100, 23); + btnItemB.TabIndex = 1; + btnItemB.Text = "Item B"; + btnItemB.UseVisualStyleBackColor = true; + // + // btnItemC + // + btnItemC.Location = new Point(215, 3); + btnItemC.Name = "btnItemC"; + btnItemC.Size = new Size(100, 23); + btnItemC.TabIndex = 2; + btnItemC.Text = "Item C"; + btnItemC.UseVisualStyleBackColor = true; + // + // btnReverse + // + btnReverse.Location = new Point(12, 215); + btnReverse.Name = "btnReverse"; + btnReverse.Size = new Size(120, 23); + btnReverse.TabIndex = 8; + btnReverse.Text = "Reverse"; + btnReverse.UseVisualStyleBackColor = true; + btnReverse.Click += btnReverse_Click; + // + // btnOpenDialog + // + btnOpenDialog.Location = new Point(140, 215); + btnOpenDialog.Name = "btnOpenDialog"; + btnOpenDialog.Size = new Size(120, 23); + btnOpenDialog.TabIndex = 9; + btnOpenDialog.Text = "Open dialog"; + btnOpenDialog.UseVisualStyleBackColor = true; + btnOpenDialog.Click += btnOpenDialog_Click; + // + // tabMain + // + tabMain.Controls.Add(tabPage1); + tabMain.Controls.Add(tabPage2); + tabMain.Location = new Point(12, 250); + tabMain.Name = "tabMain"; + tabMain.SelectedIndex = 0; + tabMain.Size = new Size(336, 80); + tabMain.TabIndex = 10; + // + // tabPage1 + // + tabPage1.Controls.Add(lblTab1); + tabPage1.Location = new Point(4, 24); + tabPage1.Name = "tabPage1"; + tabPage1.Padding = new Padding(3); + tabPage1.Size = new Size(328, 52); + tabPage1.TabIndex = 0; + tabPage1.Text = "Tab 1"; + tabPage1.UseVisualStyleBackColor = true; + // + // lblTab1 + // + lblTab1.AutoSize = true; + lblTab1.Location = new Point(6, 6); + lblTab1.Name = "lblTab1"; + lblTab1.Size = new Size(72, 15); + lblTab1.TabIndex = 0; + lblTab1.Text = "tab1 content"; + // + // tabPage2 + // + tabPage2.Controls.Add(lblTab2); + tabPage2.Location = new Point(4, 24); + tabPage2.Name = "tabPage2"; + tabPage2.Padding = new Padding(3); + tabPage2.Size = new Size(328, 52); + tabPage2.TabIndex = 1; + tabPage2.Text = "Tab 2"; + tabPage2.UseVisualStyleBackColor = true; + // + // lblTab2 + // + lblTab2.AutoSize = true; + lblTab2.Location = new Point(6, 6); + lblTab2.Name = "lblTab2"; + lblTab2.Size = new Size(72, 15); + lblTab2.TabIndex = 0; + lblTab2.Text = "tab2 content"; + // + // MainForm + // + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(360, 340); + Controls.Add(lblResult); + Controls.Add(btnClick); + Controls.Add(txtInput); + Controls.Add(chkToggle); + Controls.Add(lstItems); + Controls.Add(btnRecreate); + Controls.Add(btnVolatile); + Controls.Add(pnlItems); + Controls.Add(btnReverse); + Controls.Add(btnOpenDialog); + Controls.Add(tabMain); + Name = "MainForm"; + StartPosition = FormStartPosition.CenterScreen; + Text = "WinUia Test App"; + TopMost = true; + pnlItems.ResumeLayout(false); + tabMain.ResumeLayout(false); + tabPage1.ResumeLayout(false); + tabPage1.PerformLayout(); + tabPage2.ResumeLayout(false); + tabPage2.PerformLayout(); + ResumeLayout(false); + PerformLayout(); + } + + #endregion + + private Label lblResult; + private Button btnClick; + private TextBox txtInput; + private CheckBox chkToggle; + private ListBox lstItems; + private Button btnRecreate; + private Button btnVolatile; + private FlowLayoutPanel pnlItems; + private Button btnItemA; + private Button btnItemB; + private Button btnItemC; + private Button btnReverse; + private Button btnOpenDialog; + private TabControl tabMain; + private TabPage tabPage1; + private Label lblTab1; + private TabPage tabPage2; + private Label lblTab2; + } +} diff --git a/examples/WinUia.Winforms/MainForm.cs b/examples/WinUia.Winforms/MainForm.cs new file mode 100644 index 0000000..19bd000 --- /dev/null +++ b/examples/WinUia.Winforms/MainForm.cs @@ -0,0 +1,68 @@ +namespace WinUia.Winforms; + +/// +/// Fixture window for the WinUia UI tests. The controls are laid out in the designer; WinForms exposes each +/// control's as its UI Automation AutomationId, so those names are the ids the tests +/// search for. The window is TopMost because the physical-input tests click screen coordinates. +/// +public partial class MainForm : Form +{ + private int _generation = 1; + + public MainForm() + { + InitializeComponent(); + } + + private void btnClick_Click(object? sender, EventArgs e) => lblResult.Text = "Clicked"; + + /// Opens the modal DialogForm and reports how it was closed in lblResult. + private void btnOpenDialog_Click(object? sender, EventArgs e) + { + using var dialog = new DialogForm(); + var result = dialog.ShowDialog(this); + lblResult.Text = result == DialogResult.OK ? $"Dialog: OK ({dialog.InputText})" : $"Dialog: {result}"; + } + + /// + /// Destroys btnVolatile and creates a new one with the same AutomationId, which makes elements found earlier + /// stale (their window handle is gone) — the WinUI 3 re-render case. + /// + private void btnRecreate_Click(object? sender, EventArgs e) + { + var old = btnVolatile; + btnVolatile = Recreate(old, $"Volatile {++_generation}"); + Controls.Remove(old); + old.Dispose(); + Controls.Add(btnVolatile); + } + + /// + /// Recreates the buttons in pnlItems in reverse order: every button gets a new window handle and most change + /// position, so index-based re-resolution would pick the wrong one. + /// + private void btnReverse_Click(object? sender, EventArgs e) + { + var old = pnlItems.Controls.Cast