From 9b849a220c2a8cd9122d01cc4a704402e1e207d3 Mon Sep 17 00:00:00 2001 From: Jelle Buning Date: Mon, 5 Oct 2026 10:33:03 +0200 Subject: [PATCH 1/9] Update repo URLs and NuGet publishing --- .github/README.md | 26 +++++++++++++------------- .github/workflows/publish.yaml | 20 +++++++++++++++++--- src/Directory.Build.props | 4 ++-- 3 files changed, 32 insertions(+), 18 deletions(-) diff --git a/.github/README.md b/.github/README.md index f808c70..0ac46e0 100644 --- a/.github/README.md +++ b/.github/README.md @@ -11,9 +11,9 @@

A .NET library for automating Windows applications through Microsoft UI Automation.
- Report Bug + Report Bug · - Request Feature + Request Feature

@@ -72,7 +72,7 @@ Setting up WinUia on your local machine is straightforward. Make sure the [.NET 1. **Clone the repository:** ```bash - git clone https://github.com/BuningSoftware/win-uia.git + git clone https://github.com/buning-software/win-uia.git cd win-uia ``` @@ -206,13 +206,13 @@ Distributed under the GNU Affero General Public License v3.0 License. See `LICEN -[contributors-shield]: https://img.shields.io/github/contributors/BuningSoftware/win-uia.svg?style=for-the-badge -[contributors-url]: https://github.com/BuningSoftware/win-uia/graphs/contributors -[forks-shield]: https://img.shields.io/github/forks/BuningSoftware/win-uia.svg?style=for-the-badge -[forks-url]: https://github.com/BuningSoftware/win-uia/network/members -[stars-shield]: https://img.shields.io/github/stars/BuningSoftware/win-uia.svg?style=for-the-badge -[stars-url]: https://github.com/BuningSoftware/win-uia/stargazers -[issues-shield]: https://img.shields.io/github/issues/BuningSoftware/win-uia.svg?style=for-the-badge -[issues-url]: https://github.com/BuningSoftware/win-uia/issues -[license-shield]: https://img.shields.io/github/license/BuningSoftware/win-uia.svg?style=for-the-badge -[license-url]: https://github.com/BuningSoftware/win-uia/blob/main/LICENSE +[contributors-shield]: https://img.shields.io/github/contributors/buning-software/win-uia.svg?style=for-the-badge +[contributors-url]: https://github.com/buning-software/win-uia/graphs/contributors +[forks-shield]: https://img.shields.io/github/forks/buning-software/win-uia.svg?style=for-the-badge +[forks-url]: https://github.com/buning-software/win-uia/network/members +[stars-shield]: https://img.shields.io/github/stars/buning-software/win-uia.svg?style=for-the-badge +[stars-url]: https://github.com/buning-software/win-uia/stargazers +[issues-shield]: https://img.shields.io/github/issues/buning-software/win-uia.svg?style=for-the-badge +[issues-url]: https://github.com/buning-software/win-uia/issues +[license-shield]: https://img.shields.io/github/license/buning-software/win-uia.svg?style=for-the-badge +[license-url]: https://github.com/buning-software/win-uia/blob/main/LICENSE diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index ddf8693..6cbecf2 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -1,7 +1,10 @@ name: Publish # Publishing a GitHub release publishes the WinUia packages to nuget.org, versioned from the release tag: -# v1.2.0 -> 1.2.0, v1.3.0-beta.1 -> 1.3.0-beta.1. +# v1.2.0 or V1.2.0 -> 1.2.0, v1.3.0-beta.1 -> 1.3.0-beta.1. +# Authenticates with NuGet trusted publishing: no long-lived API key is stored anywhere. nuget.org trusts this workflow +# (owner buning-software, repository win-uia, workflow publish.yaml, environment nuget) and exchanges its GitHub OIDC +# token for a short-lived API key. The nuget environment holds one secret, NUGET_USER: the nuget.org account name. on: release: types: [published] @@ -20,6 +23,10 @@ jobs: needs: test runs-on: windows-latest environment: nuget + permissions: + contents: read + # Lets NuGet/login request the GitHub OIDC token that nuget.org exchanges for a temporary API key. + id-token: write steps: - name: Checkout uses: actions/checkout@v7 @@ -35,7 +42,7 @@ jobs: env: TAG: ${{ github.event.release.tag_name }} run: | - version="${TAG#v}" + version="${TAG#[vV]}" if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then echo "::error::Release tag '$TAG' is not a version such as v1.2.0 or v1.3.0-beta.1." exit 1 @@ -55,11 +62,18 @@ jobs: path: packages if-no-files-found: error + # The temporary API key is valid for an hour, so log in right before the push. + - name: Log in to nuget.org + id: nuget-login + uses: NuGet/login@v1 + with: + user: ${{ secrets.NUGET_USER }} + # Pushes every .nupkg; nuget.org picks up the .snupkg symbol package next to each one. - name: Push to nuget.org shell: bash run: >- dotnet nuget push "packages/*.nupkg" - --api-key "${{ secrets.NUGET_API_KEY }}" + --api-key "${{ steps.nuget-login.outputs.NUGET_API_KEY }}" --source https://api.nuget.org/v3/index.json --skip-duplicate diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 5cbfcb6..72358a2 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -15,8 +15,8 @@ Buning Software Buning Software Copyright © Buning Software - https://github.com/BuningSoftware/win-uia - https://github.com/BuningSoftware/win-uia + https://github.com/buning-software/win-uia + https://github.com/buning-software/win-uia git ui-automation;uia;uia3;windows;automation;testing;ui-testing;winforms;wpf;winui README.md From 97b1fa8dd91ba5cce10cf261c97f2f5f4971694a Mon Sep 17 00:00:00 2001 From: Jelle Buning Date: Mon, 5 Oct 2026 10:46:30 +0200 Subject: [PATCH 2/9] Update publish.yaml --- .github/workflows/publish.yaml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 6cbecf2..2a08981 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -69,11 +69,14 @@ jobs: with: user: ${{ secrets.NUGET_USER }} - # Pushes every .nupkg; nuget.org picks up the .snupkg symbol package next to each one. + # Pushes every .nupkg; nuget.org picks up the .snupkg symbol package next to each one. Bash expands the glob: + # dotnet nuget push on Windows does not expand "packages/*.nupkg" itself and fails with "File does not exist". - name: Push to nuget.org shell: bash - run: >- - dotnet nuget push "packages/*.nupkg" - --api-key "${{ steps.nuget-login.outputs.NUGET_API_KEY }}" - --source https://api.nuget.org/v3/index.json - --skip-duplicate + env: + NUGET_API_KEY: ${{ steps.nuget-login.outputs.NUGET_API_KEY }} + run: | + for package in packages/*.nupkg; do + dotnet nuget push "$package" --api-key "$NUGET_API_KEY" \ + --source https://api.nuget.org/v3/index.json --skip-duplicate + done From a4114baa26c84128e4d96e24573b541e60389209 Mon Sep 17 00:00:00 2001 From: Jelle Buning Date: Mon, 5 Oct 2026 11:35:12 +0200 Subject: [PATCH 3/9] Update Directory.Build.props --- src/Directory.Build.props | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 72358a2..1020491 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -19,17 +19,11 @@ https://github.com/buning-software/win-uia git ui-automation;uia;uia3;windows;automation;testing;ui-testing;winforms;wpf;winui - README.md true snupkg - - - - - From f2dab13fe0ed60f9466eb005152c0460f4fa880b Mon Sep 17 00:00:00 2001 From: Jelle Buning Date: Mon, 5 Oct 2026 13:29:56 +0200 Subject: [PATCH 4/9] Rename WinForms example tests to UiTests --- .github/README.md | 6 +++--- WinUia.slnx | 2 +- .../Application/DialogForm.cs | 3 +-- .../Application/Tab1.cs | 2 +- .../Application/Tab2.cs | 2 +- .../Application/WinFormsApp.cs | 2 +- .../Tests}/DialogTests.cs | 4 ++-- .../Tests}/MainFormTests.cs | 4 ++-- .../Tests}/PatternTests.cs | 6 +++--- .../Tests}/SelfHealingTests.cs | 6 +++--- .../Tests}/TabTests.cs | 4 ++-- .../WinUia.Examples.Winforms.UiTests.csproj} | 0 examples/WinUia.Examples.Winforms/MainForm.cs | 2 +- 13 files changed, 21 insertions(+), 22 deletions(-) rename examples/{WinUia.Examples.Winforms.Tests => WinUia.Examples.Winforms.UiTests}/Application/DialogForm.cs (88%) rename examples/{WinUia.Examples.Winforms.Tests => WinUia.Examples.Winforms.UiTests}/Application/Tab1.cs (79%) rename examples/{WinUia.Examples.Winforms.Tests => WinUia.Examples.Winforms.UiTests}/Application/Tab2.cs (79%) rename examples/{WinUia.Examples.Winforms.Tests => WinUia.Examples.Winforms.UiTests}/Application/WinFormsApp.cs (97%) rename examples/{WinUia.Examples.Winforms.Tests => WinUia.Examples.Winforms.UiTests/Tests}/DialogTests.cs (95%) rename examples/{WinUia.Examples.Winforms.Tests => WinUia.Examples.Winforms.UiTests/Tests}/MainFormTests.cs (96%) rename examples/{WinUia.Examples.Winforms.Tests => WinUia.Examples.Winforms.UiTests/Tests}/PatternTests.cs (98%) rename examples/{WinUia.Examples.Winforms.Tests => WinUia.Examples.Winforms.UiTests/Tests}/SelfHealingTests.cs (97%) rename examples/{WinUia.Examples.Winforms.Tests => WinUia.Examples.Winforms.UiTests/Tests}/TabTests.cs (95%) rename examples/{WinUia.Examples.Winforms.Tests/WinUia.Examples.Winforms.Tests.csproj => WinUia.Examples.Winforms.UiTests/WinUia.Examples.Winforms.UiTests.csproj} (100%) diff --git a/.github/README.md b/.github/README.md index 0ac46e0..c7eff32 100644 --- a/.github/README.md +++ b/.github/README.md @@ -118,7 +118,7 @@ A page object for the whole application derives from `App` and lists its control which executable it is and how it starts by default, so tests launch it without repeating either. `AppLaunchOptions` sets the arguments, working directory, environment, pointer and main-window timeout; options passed to a factory override the page object's defaults one by one. Page objects for part of the UI are plain classes built on an -`Element` (see `examples/WinUia.Examples.Winforms.Tests`): +`Element` (see `examples/WinUia.Examples.Winforms.UiTests`): ```csharp public sealed class MainPage : App @@ -167,10 +167,10 @@ Every test in a `.UiTests` project gets `[UiTest]` from the build (category `UI` `dotnet test --filter "Category!=UI"` runs the unit and integration tests on any Windows machine. UI tests launch `tests/WinUia.TestApp`, an empty window, located through `tests/WinUia.Testing.Shared` (`AppPaths`). Tests of control behaviour (patterns, searches, self-healing elements, dialogs, input) run against the WinForms example instead, in -`examples/WinUia.Examples.Winforms.Tests`. Tests use NUnit. +`examples/WinUia.Examples.Winforms.UiTests`. Tests use NUnit. `examples/` shows WinUia the way a user would use it: `WinUia.Examples.Winforms` is a WinForms app and -`WinUia.Examples.Winforms.Tests` tests it with page objects, using only WinUia's public API and `WinUia.NUnit`, nothing +`WinUia.Examples.Winforms.UiTests` tests it with page objects, using only WinUia's public API and `WinUia.NUnit`, nothing from `tests/`. Examples and tests are independent: neither references the other. ### Limitations diff --git a/WinUia.slnx b/WinUia.slnx index 320f723..0650e97 100644 --- a/WinUia.slnx +++ b/WinUia.slnx @@ -22,7 +22,7 @@ + - diff --git a/examples/WinUia.Examples.Winforms.Tests/Application/DialogForm.cs b/examples/WinUia.Examples.Winforms.UiTests/Application/DialogForm.cs similarity index 88% rename from examples/WinUia.Examples.Winforms.Tests/Application/DialogForm.cs rename to examples/WinUia.Examples.Winforms.UiTests/Application/DialogForm.cs index a28e525..f7c2783 100644 --- a/examples/WinUia.Examples.Winforms.Tests/Application/DialogForm.cs +++ b/examples/WinUia.Examples.Winforms.UiTests/Application/DialogForm.cs @@ -1,7 +1,6 @@ using WinUia.Core; - -namespace WinUia.Examples.Winforms.Tests.Application; +namespace WinUia.Examples.Winforms.UiTests.Application; public sealed class DialogForm(Element window) { diff --git a/examples/WinUia.Examples.Winforms.Tests/Application/Tab1.cs b/examples/WinUia.Examples.Winforms.UiTests/Application/Tab1.cs similarity index 79% rename from examples/WinUia.Examples.Winforms.Tests/Application/Tab1.cs rename to examples/WinUia.Examples.Winforms.UiTests/Application/Tab1.cs index 847eef1..8f83934 100644 --- a/examples/WinUia.Examples.Winforms.Tests/Application/Tab1.cs +++ b/examples/WinUia.Examples.Winforms.UiTests/Application/Tab1.cs @@ -1,6 +1,6 @@ using WinUia.Core; -namespace WinUia.Examples.Winforms.Tests.Application; +namespace WinUia.Examples.Winforms.UiTests.Application; /// The content of the first tab page, found under the main window. public sealed class Tab1(Element window) diff --git a/examples/WinUia.Examples.Winforms.Tests/Application/Tab2.cs b/examples/WinUia.Examples.Winforms.UiTests/Application/Tab2.cs similarity index 79% rename from examples/WinUia.Examples.Winforms.Tests/Application/Tab2.cs rename to examples/WinUia.Examples.Winforms.UiTests/Application/Tab2.cs index 43a2bbe..f587578 100644 --- a/examples/WinUia.Examples.Winforms.Tests/Application/Tab2.cs +++ b/examples/WinUia.Examples.Winforms.UiTests/Application/Tab2.cs @@ -1,6 +1,6 @@ using WinUia.Core; -namespace WinUia.Examples.Winforms.Tests.Application; +namespace WinUia.Examples.Winforms.UiTests.Application; /// The content of the second tab page, found under the main window. public sealed class Tab2(Element window) diff --git a/examples/WinUia.Examples.Winforms.Tests/Application/WinFormsApp.cs b/examples/WinUia.Examples.Winforms.UiTests/Application/WinFormsApp.cs similarity index 97% rename from examples/WinUia.Examples.Winforms.Tests/Application/WinFormsApp.cs rename to examples/WinUia.Examples.Winforms.UiTests/Application/WinFormsApp.cs index 390533a..1560651 100644 --- a/examples/WinUia.Examples.Winforms.Tests/Application/WinFormsApp.cs +++ b/examples/WinUia.Examples.Winforms.UiTests/Application/WinFormsApp.cs @@ -1,7 +1,7 @@ using System.Reflection; using WinUia.Core; -namespace WinUia.Examples.Winforms.Tests.Application; +namespace WinUia.Examples.Winforms.UiTests.Application; /// /// Page object for the example WinForms app: the itself, with the controls of its main window. diff --git a/examples/WinUia.Examples.Winforms.Tests/DialogTests.cs b/examples/WinUia.Examples.Winforms.UiTests/Tests/DialogTests.cs similarity index 95% rename from examples/WinUia.Examples.Winforms.Tests/DialogTests.cs rename to examples/WinUia.Examples.Winforms.UiTests/Tests/DialogTests.cs index 6b8a048..316569e 100644 --- a/examples/WinUia.Examples.Winforms.Tests/DialogTests.cs +++ b/examples/WinUia.Examples.Winforms.UiTests/Tests/DialogTests.cs @@ -1,8 +1,8 @@ using System.Diagnostics; using WinUia.Core.Exceptions; -using WinUia.Examples.Winforms.Tests.Application; +using WinUia.Examples.Winforms.UiTests.Application; -namespace WinUia.Examples.Winforms.Tests; +namespace WinUia.Examples.Winforms.UiTests; [UiTest] public sealed class DialogTests diff --git a/examples/WinUia.Examples.Winforms.Tests/MainFormTests.cs b/examples/WinUia.Examples.Winforms.UiTests/Tests/MainFormTests.cs similarity index 96% rename from examples/WinUia.Examples.Winforms.Tests/MainFormTests.cs rename to examples/WinUia.Examples.Winforms.UiTests/Tests/MainFormTests.cs index a68b5ce..de2fd8f 100644 --- a/examples/WinUia.Examples.Winforms.Tests/MainFormTests.cs +++ b/examples/WinUia.Examples.Winforms.UiTests/Tests/MainFormTests.cs @@ -1,8 +1,8 @@ using WinUia.Core; using WinUia.Core.Patterns; -using WinUia.Examples.Winforms.Tests.Application; +using WinUia.Examples.Winforms.UiTests.Application; -namespace WinUia.Examples.Winforms.Tests; +namespace WinUia.Examples.Winforms.UiTests; [UiTest] public sealed class MainFormTests diff --git a/examples/WinUia.Examples.Winforms.Tests/PatternTests.cs b/examples/WinUia.Examples.Winforms.UiTests/Tests/PatternTests.cs similarity index 98% rename from examples/WinUia.Examples.Winforms.Tests/PatternTests.cs rename to examples/WinUia.Examples.Winforms.UiTests/Tests/PatternTests.cs index eb163d2..2c7f224 100644 --- a/examples/WinUia.Examples.Winforms.Tests/PatternTests.cs +++ b/examples/WinUia.Examples.Winforms.UiTests/Tests/PatternTests.cs @@ -1,9 +1,9 @@ +using WinUia.Core; using WinUia.Core.Exceptions; using WinUia.Core.Patterns; -using WinUia.Core; -using WinUia.Examples.Winforms.Tests.Application; +using WinUia.Examples.Winforms.UiTests.Application; -namespace WinUia.Examples.Winforms.Tests; +namespace WinUia.Examples.Winforms.UiTests; /// Control patterns, searches, tree navigation and physical input, exercised on the example app's controls. diff --git a/examples/WinUia.Examples.Winforms.Tests/SelfHealingTests.cs b/examples/WinUia.Examples.Winforms.UiTests/Tests/SelfHealingTests.cs similarity index 97% rename from examples/WinUia.Examples.Winforms.Tests/SelfHealingTests.cs rename to examples/WinUia.Examples.Winforms.UiTests/Tests/SelfHealingTests.cs index 2fb9f46..c939e30 100644 --- a/examples/WinUia.Examples.Winforms.Tests/SelfHealingTests.cs +++ b/examples/WinUia.Examples.Winforms.UiTests/Tests/SelfHealingTests.cs @@ -1,8 +1,8 @@ -using WinUia.Core.Exceptions; using WinUia.Core; -using WinUia.Examples.Winforms.Tests.Application; +using WinUia.Core.Exceptions; +using WinUia.Examples.Winforms.UiTests.Application; -namespace WinUia.Examples.Winforms.Tests; +namespace WinUia.Examples.Winforms.UiTests; /// /// Elements re-find themselves when their control is re-created: FindFirst results through their search, FindAll diff --git a/examples/WinUia.Examples.Winforms.Tests/TabTests.cs b/examples/WinUia.Examples.Winforms.UiTests/Tests/TabTests.cs similarity index 95% rename from examples/WinUia.Examples.Winforms.Tests/TabTests.cs rename to examples/WinUia.Examples.Winforms.UiTests/Tests/TabTests.cs index 13e9891..628240c 100644 --- a/examples/WinUia.Examples.Winforms.Tests/TabTests.cs +++ b/examples/WinUia.Examples.Winforms.UiTests/Tests/TabTests.cs @@ -1,6 +1,6 @@ -using WinUia.Examples.Winforms.Tests.Application; +using WinUia.Examples.Winforms.UiTests.Application; -namespace WinUia.Examples.Winforms.Tests; +namespace WinUia.Examples.Winforms.UiTests; [UiTest] public sealed class TabTests diff --git a/examples/WinUia.Examples.Winforms.Tests/WinUia.Examples.Winforms.Tests.csproj b/examples/WinUia.Examples.Winforms.UiTests/WinUia.Examples.Winforms.UiTests.csproj similarity index 100% rename from examples/WinUia.Examples.Winforms.Tests/WinUia.Examples.Winforms.Tests.csproj rename to examples/WinUia.Examples.Winforms.UiTests/WinUia.Examples.Winforms.UiTests.csproj diff --git a/examples/WinUia.Examples.Winforms/MainForm.cs b/examples/WinUia.Examples.Winforms/MainForm.cs index 7c1d6cc..02ffdcb 100644 --- a/examples/WinUia.Examples.Winforms/MainForm.cs +++ b/examples/WinUia.Examples.Winforms/MainForm.cs @@ -1,7 +1,7 @@ namespace WinUia.Examples.Winforms; /// -/// Main window of the WinForms example app, automated by examples/WinUia.Examples.Winforms.Tests. The controls are laid out in the designer; WinForms exposes each +/// Main window of the WinForms example app, automated by examples/WinUia.Examples.Winforms.UiTests. 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. /// From ec4ad3150eb294b9247a026e8f30c0390bfea58a Mon Sep 17 00:00:00 2001 From: Jelle Buning Date: Mon, 5 Oct 2026 13:39:06 +0200 Subject: [PATCH 5/9] Update publish.yaml --- .github/workflows/publish.yaml | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 2a08981..94586f1 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -1,10 +1,12 @@ name: Publish -# Publishing a GitHub release publishes the WinUia packages to nuget.org, versioned from the release tag: -# v1.2.0 or V1.2.0 -> 1.2.0, v1.3.0-beta.1 -> 1.3.0-beta.1. +# Publishing a GitHub release publishes the WinUia packages to GitHub Packages and nuget.org, versioned from the +# release tag: v1.2.0 or V1.2.0 -> 1.2.0, v1.3.0-beta.1 -> 1.3.0-beta.1. # Authenticates with NuGet trusted publishing: no long-lived API key is stored anywhere. nuget.org trusts this workflow # (owner buning-software, repository win-uia, workflow publish.yaml, environment nuget) and exchanges its GitHub OIDC # token for a short-lived API key. The nuget environment holds one secret, NUGET_USER: the nuget.org account name. +# GitHub Packages is pushed to with the workflow's own GITHUB_TOKEN; it links each package to this repository through +# its RepositoryUrl, so they show under the repository's Packages. on: release: types: [published] @@ -25,6 +27,8 @@ jobs: environment: nuget permissions: contents: read + # Lets the GITHUB_TOKEN push to GitHub Packages. + packages: write # Lets NuGet/login request the GitHub OIDC token that nuget.org exchanges for a temporary API key. id-token: write steps: @@ -62,6 +66,17 @@ jobs: path: packages if-no-files-found: error + # GitHub Packages has no symbol server, so only the .nupkg files go there. + - name: Push to GitHub Packages + shell: bash + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + for package in packages/*.nupkg; do + dotnet nuget push "$package" --api-key "$GITHUB_TOKEN" --no-symbols \ + --source "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json" --skip-duplicate + done + # The temporary API key is valid for an hour, so log in right before the push. - name: Log in to nuget.org id: nuget-login From 92538d98562cf696c8869aef386b37819f7446fc Mon Sep 17 00:00:00 2001 From: Jelle Buning Date: Mon, 5 Oct 2026 16:12:50 +0200 Subject: [PATCH 6/9] . --- examples/WinUia.Examples.Winforms.UiTests/Application/Tab1.cs | 2 +- examples/WinUia.Examples.Winforms.UiTests/Application/Tab2.cs | 2 +- .../Application/WinFormsApp.cs | 2 +- .../WinUia.Examples.Winforms.UiTests/Tests/DialogTests.cs | 2 +- .../WinUia.Examples.Winforms.UiTests/Tests/MainFormTests.cs | 2 +- .../WinUia.Examples.Winforms.UiTests/Tests/PatternTests.cs | 2 +- .../Tests/SelfHealingTests.cs | 2 +- examples/WinUia.Examples.Winforms.UiTests/Tests/TabTests.cs | 4 ++-- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/WinUia.Examples.Winforms.UiTests/Application/Tab1.cs b/examples/WinUia.Examples.Winforms.UiTests/Application/Tab1.cs index 8f83934..3212190 100644 --- a/examples/WinUia.Examples.Winforms.UiTests/Application/Tab1.cs +++ b/examples/WinUia.Examples.Winforms.UiTests/Application/Tab1.cs @@ -3,7 +3,7 @@ namespace WinUia.Examples.Winforms.UiTests.Application; /// The content of the first tab page, found under the main window. -public sealed class Tab1(Element window) +public class Tab1(Element window) : WinFormsApp { public Element Content => window.FindByAutomationId("lblTab1"); } diff --git a/examples/WinUia.Examples.Winforms.UiTests/Application/Tab2.cs b/examples/WinUia.Examples.Winforms.UiTests/Application/Tab2.cs index f587578..df14aee 100644 --- a/examples/WinUia.Examples.Winforms.UiTests/Application/Tab2.cs +++ b/examples/WinUia.Examples.Winforms.UiTests/Application/Tab2.cs @@ -3,7 +3,7 @@ namespace WinUia.Examples.Winforms.UiTests.Application; /// The content of the second tab page, found under the main window. -public sealed class Tab2(Element window) +public class Tab2(Element window) : WinFormsApp { public Element Content => window.FindByAutomationId("lblTab2"); } diff --git a/examples/WinUia.Examples.Winforms.UiTests/Application/WinFormsApp.cs b/examples/WinUia.Examples.Winforms.UiTests/Application/WinFormsApp.cs index 1560651..d825c86 100644 --- a/examples/WinUia.Examples.Winforms.UiTests/Application/WinFormsApp.cs +++ b/examples/WinUia.Examples.Winforms.UiTests/Application/WinFormsApp.cs @@ -9,7 +9,7 @@ namespace WinUia.Examples.Winforms.UiTests.Application; /// App.Launch<WinFormsApp>(new AppLaunchOptions { ShowPointer = false }) where nobody watches. /// The tab pages and the dialog are page objects of their own, built on the elements they cover. /// -public sealed class WinFormsApp : App +public class WinFormsApp : App { /// protected override string ExecutablePath => typeof(WinFormsApp).Assembly.GetCustomAttributes() diff --git a/examples/WinUia.Examples.Winforms.UiTests/Tests/DialogTests.cs b/examples/WinUia.Examples.Winforms.UiTests/Tests/DialogTests.cs index 316569e..bee2488 100644 --- a/examples/WinUia.Examples.Winforms.UiTests/Tests/DialogTests.cs +++ b/examples/WinUia.Examples.Winforms.UiTests/Tests/DialogTests.cs @@ -2,7 +2,7 @@ using WinUia.Core.Exceptions; using WinUia.Examples.Winforms.UiTests.Application; -namespace WinUia.Examples.Winforms.UiTests; +namespace WinUia.Examples.Winforms.UiTests.Tests; [UiTest] public sealed class DialogTests diff --git a/examples/WinUia.Examples.Winforms.UiTests/Tests/MainFormTests.cs b/examples/WinUia.Examples.Winforms.UiTests/Tests/MainFormTests.cs index de2fd8f..f0c9cd8 100644 --- a/examples/WinUia.Examples.Winforms.UiTests/Tests/MainFormTests.cs +++ b/examples/WinUia.Examples.Winforms.UiTests/Tests/MainFormTests.cs @@ -2,7 +2,7 @@ using WinUia.Core.Patterns; using WinUia.Examples.Winforms.UiTests.Application; -namespace WinUia.Examples.Winforms.UiTests; +namespace WinUia.Examples.Winforms.UiTests.Tests; [UiTest] public sealed class MainFormTests diff --git a/examples/WinUia.Examples.Winforms.UiTests/Tests/PatternTests.cs b/examples/WinUia.Examples.Winforms.UiTests/Tests/PatternTests.cs index 2c7f224..d2b10ba 100644 --- a/examples/WinUia.Examples.Winforms.UiTests/Tests/PatternTests.cs +++ b/examples/WinUia.Examples.Winforms.UiTests/Tests/PatternTests.cs @@ -3,7 +3,7 @@ using WinUia.Core.Patterns; using WinUia.Examples.Winforms.UiTests.Application; -namespace WinUia.Examples.Winforms.UiTests; +namespace WinUia.Examples.Winforms.UiTests.Tests; /// Control patterns, searches, tree navigation and physical input, exercised on the example app's controls. diff --git a/examples/WinUia.Examples.Winforms.UiTests/Tests/SelfHealingTests.cs b/examples/WinUia.Examples.Winforms.UiTests/Tests/SelfHealingTests.cs index c939e30..04f3c81 100644 --- a/examples/WinUia.Examples.Winforms.UiTests/Tests/SelfHealingTests.cs +++ b/examples/WinUia.Examples.Winforms.UiTests/Tests/SelfHealingTests.cs @@ -2,7 +2,7 @@ using WinUia.Core.Exceptions; using WinUia.Examples.Winforms.UiTests.Application; -namespace WinUia.Examples.Winforms.UiTests; +namespace WinUia.Examples.Winforms.UiTests.Tests; /// /// Elements re-find themselves when their control is re-created: FindFirst results through their search, FindAll diff --git a/examples/WinUia.Examples.Winforms.UiTests/Tests/TabTests.cs b/examples/WinUia.Examples.Winforms.UiTests/Tests/TabTests.cs index 628240c..1f8fd2f 100644 --- a/examples/WinUia.Examples.Winforms.UiTests/Tests/TabTests.cs +++ b/examples/WinUia.Examples.Winforms.UiTests/Tests/TabTests.cs @@ -1,6 +1,6 @@ using WinUia.Examples.Winforms.UiTests.Application; -namespace WinUia.Examples.Winforms.UiTests; +namespace WinUia.Examples.Winforms.UiTests.Tests; [UiTest] public sealed class TabTests @@ -57,7 +57,7 @@ public void Switching_tabs_hides_the_other_tabs_content() public void The_main_form_controls_keep_working_while_another_tab_is_selected() { var tab2 = _app.SelectTab2(); - + tab2.SelectTab1(); _app.Button.Click(); using (Assert.EnterMultipleScope()) From cfe52100040cb30a71c2ad0e9388d3b984c4bc64 Mon Sep 17 00:00:00 2001 From: Jelle Buning Date: Tue, 6 Oct 2026 20:45:06 +0200 Subject: [PATCH 7/9] Restructure WinUia modules and public APIs --- .github/README.md | 69 +++-- .github/actions/setup/action.yaml | 23 ++ .github/workflows/build-and-test.yaml | 59 +--- .github/workflows/publish.yaml | 20 +- Directory.Build.props | 1 - Directory.Packages.props | 2 +- WinUia.slnx | 4 - examples/Directory.Build.props | 4 - .../Application/Tab1.cs | 9 - .../Application/Tab2.cs | 9 - .../Application/{ => Views}/DialogForm.cs | 4 +- .../Application/Views/MainForm.cs | 38 +++ .../Application/Views/Tab1.cs | 6 + .../Application/Views/Tab2.cs | 6 + .../Application/WinFormsApp.cs | 50 +--- .../Fixtures/WinFormsFixture.cs | 20 ++ .../Tests/DialogTests.cs | 29 +- .../Tests/MainFormTests.cs | 56 ++-- .../Tests/PatternTests.cs | 108 ++++---- .../Tests/SelfHealingTests.cs | 42 +-- .../Tests/TabTests.cs | 40 +-- .../Tests/WinFormsAppTests.cs | 14 + .../WinUia.Examples.Winforms.UiTests.csproj | 18 +- .../WinUia.Examples.Winforms/DialogForm.cs | 2 - examples/WinUia.Examples.Winforms/MainForm.cs | 15 +- src/Directory.Build.props | 22 +- src/WinUia.Core/Element.cs | 171 ------------ src/WinUia.Core/ElementExtensions.cs | 36 --- .../{ => Enumerations}/ControlType.cs | 2 +- .../ExpandCollapseState.cs | 2 +- .../Enumerations}/MouseButton.cs | 0 .../{Patterns => Enumerations}/ToggleState.cs | 2 +- .../{ => Enumerations}/TreeScope.cs | 2 +- .../Enumerations}/VirtualKey.cs | 0 .../WindowVisualState.cs | 2 +- .../{ => Exceptions}/ComExceptionMapper.cs | 19 +- .../UiaElementNotEnabledException.cs | 7 - .../Exceptions/UiaElementNotFoundException.cs | 2 +- src/WinUia.Core/Exceptions/UiaException.cs | 2 +- .../UiaNoClickablePointException.cs | 7 - .../UiaPatternNotSupportedException.cs | 2 +- .../Exceptions/UiaStaleElementException.cs | 2 +- .../Exceptions/UiaTimeoutException.cs | 2 +- .../{ => Helpers}/ElementPredicate.cs | 33 +-- src/WinUia.Core/Helpers/Poll.cs | 41 +++ src/WinUia.Core/Helpers/Retry.cs | 22 ++ .../Input}/IInputSimulator.cs | 0 src/WinUia.Core/Input/PhysicalDpi.cs | 25 ++ .../Input}/Win32InputSimulator.cs | 14 +- src/WinUia.Core/Interop/HResults.cs | 1 - .../Interop/INPUT.cs | 2 +- src/WinUia.Core/Interop/IUIAutomation.cs | 10 +- .../Interop/IUIAutomationCondition.cs | 1 - .../Interop/IUIAutomationElement.cs | 1 - .../Interop/IUIAutomationElementArray.cs | 1 - .../IUIAutomationExpandCollapsePattern.cs | 1 - .../Interop/IUIAutomationInvokePattern.cs | 1 - .../Interop/IUIAutomationScrollItemPattern.cs | 1 - .../IUIAutomationSelectionItemPattern.cs | 1 - .../Interop/IUIAutomationTextPattern.cs | 1 - .../Interop/IUIAutomationTextRange.cs | 1 - .../Interop/IUIAutomationTogglePattern.cs | 1 - .../Interop/IUIAutomationTreeWalker.cs | 1 - .../Interop/IUIAutomationValuePattern.cs | 1 - .../Interop/IUIAutomationWindowPattern.cs | 1 - .../Interop/InputUnion.cs | 2 +- .../Interop/KEYBDINPUT.cs | 2 +- .../Interop/MOUSEINPUT.cs | 2 +- src/WinUia.Core/Interop/PatternIds.cs | 1 - src/WinUia.Core/Interop/PropertyIds.cs | 1 - src/WinUia.Core/Interop/User32.cs | 48 +++- .../{ => Model}/AutomationContext.cs | 32 ++- src/WinUia.Core/{ => Model}/Element.Find.cs | 50 ++-- .../{ => Model}/Element.Interaction.cs | 59 ++-- src/WinUia.Core/Model/Element.cs | 86 ++++++ src/WinUia.Core/Model/ElementExtensions.cs | 12 + src/WinUia.Core/Model/ElementHandle.cs | 84 ++++++ .../{ => Model}/ElementIdentity.cs | 3 +- src/WinUia.Core/{ => Model}/ElementLocator.cs | 24 +- src/WinUia.Core/{ => Model}/ScreenPoint.cs | 2 +- src/WinUia.Core/{ => Model}/ScreenRect.cs | 2 +- .../Patterns/ExpandCollapsePatternWrapper.cs | 2 +- .../Patterns/InvokePatternWrapper.cs | 2 +- src/WinUia.Core/Patterns/PatternWrapper.cs | 9 +- .../Patterns/ScrollItemPatternWrapper.cs | 8 +- .../Patterns/SelectionItemPatternWrapper.cs | 2 +- .../Patterns/TextPatternWrapper.cs | 10 +- .../Patterns/TogglePatternWrapper.cs | 2 +- .../Patterns/ValuePatternWrapper.cs | 2 +- .../Patterns/WindowPatternWrapper.cs | 2 +- src/WinUia.Core/Poll.cs | 43 --- src/WinUia.Core/PublicAPI.Shipped.txt | 1 + src/WinUia.Core/PublicAPI.Unshipped.txt | 252 ++++++++++++++++++ src/WinUia.Core/README.md | 22 ++ src/WinUia.Core/Retry.cs | 32 --- src/WinUia.Core/WinUia.Core.csproj | 8 +- src/WinUia.Input/Interop/NativeMethods.cs | 51 ---- src/WinUia.Input/Interop/POINT.cs | 10 - src/WinUia.Input/PhysicalDpi.cs | 35 --- src/WinUia.Input/WinUia.Input.csproj | 7 - .../{ => Assertions}/UiAssertions.cs | 12 +- .../{ => Attributes}/UiTestAttribute.cs | 0 src/WinUia.NUnit/DesktopLock.cs | 43 --- src/WinUia.NUnit/PublicAPI.Shipped.txt | 1 + src/WinUia.NUnit/PublicAPI.Unshipped.txt | 12 + src/WinUia.NUnit/README.md | 37 +++ src/WinUia.NUnit/Services/DesktopLock.cs | 73 +++++ src/WinUia.NUnit/WinUia.NUnit.csproj | 10 +- .../buildTransitive/WinUia.NUnit.props | 6 + src/WinUia/App.cs | 80 +++--- .../{ => Exceptions}/AppProcessException.cs | 2 +- src/WinUia/Interop/ActivateOptions.cs | 1 - .../Interop/ApplicationActivationManager.cs | 1 - .../Interop/IApplicationActivationManager.cs | 1 - src/WinUia/Launchers/AppLauncher.cs | 100 +------ src/WinUia/Launchers/PackagedAppActivator.cs | 7 +- src/WinUia/{ => Models}/AppLaunchOptions.cs | 5 +- src/WinUia/PublicAPI.Shipped.txt | 1 + src/WinUia/PublicAPI.Unshipped.txt | 45 ++++ src/WinUia/README.md | 21 ++ src/WinUia/Services/AppCloser.cs | 36 +++ src/WinUia/Services/MainWindowLocator.cs | 32 +++ tests/Directory.Build.props | 25 +- .../ElementPredicateConditionTests.cs | 4 +- .../{ => Input}/InputSimulatorTests.cs | 6 +- .../Input}/PhysicalDpiTests.cs | 2 +- .../{ => Input}/RecordingInputSimulator.cs | 3 +- .../{ => Interop}/InteropVtableTests.cs | 58 ++-- .../{ => Model}/AutomationContextTests.cs | 2 +- .../WinUia.Core.IntegrationTests.csproj | 1 - .../Input}/CursorTests.cs | 4 +- .../Model/WindowPropertyTests.cs | 33 +++ .../WinUia.Core.UiTests.csproj | 3 - .../WindowPropertyTests.cs | 38 --- .../AutomationIdValue.cs | 7 - .../ComExceptionMapperTests.cs | 7 +- .../Helpers/AutomationIdValue.cs | 6 + .../{ => Helpers}/ElementPredicateTests.cs | 2 +- .../{ => Helpers}/ProcessHolder.cs | 3 +- .../{ => Helpers}/RetryTests.cs | 5 +- .../Interop}/NativeLayoutTests.cs | 4 +- .../WinUia.Core.UnitTests.csproj | 1 - .../WinUia.Input.IntegrationTests.csproj | 8 - .../WinUia.Input.UiTests.csproj | 8 - .../WinUia.Input.UnitTests.csproj | 8 - .../{ => Launchers}/AppLauncherTests.cs | 14 +- .../{ => Model}/AppTests.cs | 4 +- .../WinUia.IntegrationTests.csproj | 1 - .../DesktopLockTests.cs | 32 --- .../Services/DesktopLockTests.cs | 63 +++++ .../WinUia.NUnit.IntegrationTests.csproj | 2 - .../{ => Attributes}/UiTestAttributeTests.cs | 4 +- .../WinUia.NUnit.UiTests.csproj | 2 - .../{ => Assertions}/UiAssertionsTests.cs | 2 +- .../WinUia.NUnit.UnitTests.csproj | 2 - tests/WinUia.TestApp/EmptyForm.cs | 1 - tests/WinUia.TestApp/Program.cs | 1 - tests/WinUia.TestApp/WinUia.TestApp.csproj | 2 - tests/WinUia.Testing.Shared/AppPaths.cs | 6 - .../WinUia.Testing.Shared.csproj | 7 - tests/WinUia.UiTests/DerivedApp.cs | 7 - .../{ => Launchers}/AppLauncherTests.cs | 26 +- tests/WinUia.UiTests/{ => Model}/AppTests.cs | 10 +- tests/WinUia.UiTests/Model/DerivedApp.cs | 6 + .../{ => Model}/LaunchableApp.cs | 4 +- tests/WinUia.UiTests/WinUia.UiTests.csproj | 2 - .../{ => Model}/AppLaunchOptionsTests.cs | 4 +- .../WinUia.UnitTests/{ => Model}/AppTests.cs | 6 +- .../WinUia.UnitTests/Model/UnconnectedApp.cs | 3 + tests/WinUia.UnitTests/UnconnectedApp.cs | 4 - .../WinUia.UnitTests/WinUia.UnitTests.csproj | 1 - 171 files changed, 1518 insertions(+), 1463 deletions(-) create mode 100644 .github/actions/setup/action.yaml delete mode 100644 examples/WinUia.Examples.Winforms.UiTests/Application/Tab1.cs delete mode 100644 examples/WinUia.Examples.Winforms.UiTests/Application/Tab2.cs rename examples/WinUia.Examples.Winforms.UiTests/Application/{ => Views}/DialogForm.cs (84%) create mode 100644 examples/WinUia.Examples.Winforms.UiTests/Application/Views/MainForm.cs create mode 100644 examples/WinUia.Examples.Winforms.UiTests/Application/Views/Tab1.cs create mode 100644 examples/WinUia.Examples.Winforms.UiTests/Application/Views/Tab2.cs create mode 100644 examples/WinUia.Examples.Winforms.UiTests/Fixtures/WinFormsFixture.cs create mode 100644 examples/WinUia.Examples.Winforms.UiTests/Tests/WinFormsAppTests.cs delete mode 100644 src/WinUia.Core/Element.cs delete mode 100644 src/WinUia.Core/ElementExtensions.cs rename src/WinUia.Core/{ => Enumerations}/ControlType.cs (98%) rename src/WinUia.Core/{Patterns => Enumerations}/ExpandCollapseState.cs (92%) rename src/{WinUia.Input => WinUia.Core/Enumerations}/MouseButton.cs (100%) rename src/WinUia.Core/{Patterns => Enumerations}/ToggleState.cs (89%) rename src/WinUia.Core/{ => Enumerations}/TreeScope.cs (96%) rename src/{WinUia.Input => WinUia.Core/Enumerations}/VirtualKey.cs (100%) rename src/WinUia.Core/{Patterns => Enumerations}/WindowVisualState.cs (89%) rename src/WinUia.Core/{ => Exceptions}/ComExceptionMapper.cs (54%) delete mode 100644 src/WinUia.Core/Exceptions/UiaElementNotEnabledException.cs delete mode 100644 src/WinUia.Core/Exceptions/UiaNoClickablePointException.cs rename src/WinUia.Core/{ => Helpers}/ElementPredicate.cs (74%) create mode 100644 src/WinUia.Core/Helpers/Poll.cs create mode 100644 src/WinUia.Core/Helpers/Retry.cs rename src/{WinUia.Input => WinUia.Core/Input}/IInputSimulator.cs (100%) create mode 100644 src/WinUia.Core/Input/PhysicalDpi.cs rename src/{WinUia.Input => WinUia.Core/Input}/Win32InputSimulator.cs (91%) rename src/{WinUia.Input => WinUia.Core}/Interop/INPUT.cs (82%) rename src/{WinUia.Input => WinUia.Core}/Interop/InputUnion.cs (85%) rename src/{WinUia.Input => WinUia.Core}/Interop/KEYBDINPUT.cs (87%) rename src/{WinUia.Input => WinUia.Core}/Interop/MOUSEINPUT.cs (88%) rename src/WinUia.Core/{ => Model}/AutomationContext.cs (77%) rename src/WinUia.Core/{ => Model}/Element.Find.cs (73%) rename src/WinUia.Core/{ => Model}/Element.Interaction.cs (77%) create mode 100644 src/WinUia.Core/Model/Element.cs create mode 100644 src/WinUia.Core/Model/ElementExtensions.cs create mode 100644 src/WinUia.Core/Model/ElementHandle.cs rename src/WinUia.Core/{ => Model}/ElementIdentity.cs (83%) rename src/WinUia.Core/{ => Model}/ElementLocator.cs (52%) rename src/WinUia.Core/{ => Model}/ScreenPoint.cs (83%) rename src/WinUia.Core/{ => Model}/ScreenRect.cs (96%) delete mode 100644 src/WinUia.Core/Poll.cs create mode 100644 src/WinUia.Core/PublicAPI.Shipped.txt create mode 100644 src/WinUia.Core/PublicAPI.Unshipped.txt create mode 100644 src/WinUia.Core/README.md delete mode 100644 src/WinUia.Core/Retry.cs delete mode 100644 src/WinUia.Input/Interop/NativeMethods.cs delete mode 100644 src/WinUia.Input/Interop/POINT.cs delete mode 100644 src/WinUia.Input/PhysicalDpi.cs delete mode 100644 src/WinUia.Input/WinUia.Input.csproj rename src/WinUia.NUnit/{ => Assertions}/UiAssertions.cs (57%) rename src/WinUia.NUnit/{ => Attributes}/UiTestAttribute.cs (100%) delete mode 100644 src/WinUia.NUnit/DesktopLock.cs create mode 100644 src/WinUia.NUnit/PublicAPI.Shipped.txt create mode 100644 src/WinUia.NUnit/PublicAPI.Unshipped.txt create mode 100644 src/WinUia.NUnit/README.md create mode 100644 src/WinUia.NUnit/Services/DesktopLock.cs create mode 100644 src/WinUia.NUnit/buildTransitive/WinUia.NUnit.props rename src/WinUia/{ => Exceptions}/AppProcessException.cs (75%) rename src/WinUia/{ => Models}/AppLaunchOptions.cs (91%) create mode 100644 src/WinUia/PublicAPI.Shipped.txt create mode 100644 src/WinUia/PublicAPI.Unshipped.txt create mode 100644 src/WinUia/README.md create mode 100644 src/WinUia/Services/AppCloser.cs create mode 100644 src/WinUia/Services/MainWindowLocator.cs rename tests/WinUia.Core.IntegrationTests/{ => Helpers}/ElementPredicateConditionTests.cs (72%) rename tests/WinUia.Core.IntegrationTests/{ => Input}/InputSimulatorTests.cs (81%) rename tests/{WinUia.Input.IntegrationTests => WinUia.Core.IntegrationTests/Input}/PhysicalDpiTests.cs (96%) rename tests/WinUia.Core.IntegrationTests/{ => Input}/RecordingInputSimulator.cs (82%) rename tests/WinUia.Core.IntegrationTests/{ => Interop}/InteropVtableTests.cs (56%) rename tests/WinUia.Core.IntegrationTests/{ => Model}/AutomationContextTests.cs (98%) rename tests/{WinUia.Input.UiTests => WinUia.Core.UiTests/Input}/CursorTests.cs (92%) create mode 100644 tests/WinUia.Core.UiTests/Model/WindowPropertyTests.cs delete mode 100644 tests/WinUia.Core.UiTests/WindowPropertyTests.cs delete mode 100644 tests/WinUia.Core.UnitTests/AutomationIdValue.cs rename tests/WinUia.Core.UnitTests/{ => Exceptions}/ComExceptionMapperTests.cs (90%) create mode 100644 tests/WinUia.Core.UnitTests/Helpers/AutomationIdValue.cs rename tests/WinUia.Core.UnitTests/{ => Helpers}/ElementPredicateTests.cs (98%) rename tests/WinUia.Core.UnitTests/{ => Helpers}/ProcessHolder.cs (60%) rename tests/WinUia.Core.UnitTests/{ => Helpers}/RetryTests.cs (92%) rename tests/{WinUia.Input.UnitTests => WinUia.Core.UnitTests/Interop}/NativeLayoutTests.cs (89%) delete mode 100644 tests/WinUia.Input.IntegrationTests/WinUia.Input.IntegrationTests.csproj delete mode 100644 tests/WinUia.Input.UiTests/WinUia.Input.UiTests.csproj delete mode 100644 tests/WinUia.Input.UnitTests/WinUia.Input.UnitTests.csproj rename tests/WinUia.IntegrationTests/{ => Launchers}/AppLauncherTests.cs (79%) rename tests/WinUia.IntegrationTests/{ => Model}/AppTests.cs (51%) delete mode 100644 tests/WinUia.NUnit.IntegrationTests/DesktopLockTests.cs create mode 100644 tests/WinUia.NUnit.IntegrationTests/Services/DesktopLockTests.cs rename tests/WinUia.NUnit.UiTests/{ => Attributes}/UiTestAttributeTests.cs (93%) rename tests/WinUia.NUnit.UnitTests/{ => Assertions}/UiAssertionsTests.cs (93%) delete mode 100644 tests/WinUia.UiTests/DerivedApp.cs rename tests/WinUia.UiTests/{ => Launchers}/AppLauncherTests.cs (69%) rename tests/WinUia.UiTests/{ => Model}/AppTests.cs (91%) create mode 100644 tests/WinUia.UiTests/Model/DerivedApp.cs rename tests/WinUia.UiTests/{ => Model}/LaunchableApp.cs (65%) rename tests/WinUia.UnitTests/{ => Model}/AppLaunchOptionsTests.cs (93%) rename tests/WinUia.UnitTests/{ => Model}/AppTests.cs (89%) create mode 100644 tests/WinUia.UnitTests/Model/UnconnectedApp.cs delete mode 100644 tests/WinUia.UnitTests/UnconnectedApp.cs diff --git a/.github/README.md b/.github/README.md index c7eff32..1c93246 100644 --- a/.github/README.md +++ b/.github/README.md @@ -32,6 +32,7 @@ Getting Started
  • @@ -65,10 +66,17 @@ WinUia is a .NET library for inspecting and automating Windows applications thro * [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. +WinUia needs the [.NET 10 SDK](https://dotnet.microsoft.com/download) and Windows. ### Installation +```bash +dotnet add package WinUia +dotnet add package WinUia.NUnit # for NUnit UI tests +``` + +### Building from source + 1. **Clone the repository:** ```bash @@ -94,7 +102,6 @@ Setting up WinUia on your local machine is straightforward. Make sure the [.NET ```csharp using WinUia; -using WinUia.Core; using var app = App.Launch(@"C:\path\to\MyApp.exe"); // or App.LaunchPackaged("Publisher.App_hash!App"), App.Attach(pid) @@ -113,47 +120,67 @@ dialog.Find(e => e.ControlType == ControlType.Button && e.Name == "Yes").Click() they check the element is enabled and pick the right pattern. The pattern wrappers (`element.TogglePattern.State`, `element.WindowPattern.Close()`, ...) are for reading pattern state and for the members those methods do not cover. -A page object for the whole application derives from `App` and lists its controls; the generic factories -(`App.Launch`, `App.LaunchPackaged`, `App.Attach`) create it already connected. A page object can also say -which executable it is and how it starts by default, so tests launch it without repeating either. `AppLaunchOptions` -sets the arguments, working directory, environment, pointer and main-window timeout; options passed to a factory -override the page object's defaults one by one. Page objects for part of the UI are plain classes built on an -`Element` (see `examples/WinUia.Examples.Winforms.UiTests`): +An application can have its own class that derives from `App` and says which executable it is and how it starts by +default, so tests launch it without repeating either; the generic factories (`App.Launch`, `App.Attach`) create it already connected. `AppLaunchOptions` sets the arguments, working directory, environment, +pointer and main-window timeout; options passed to a factory override those defaults one by one. + +The app owns the process, so it is the one thing you dispose. What is on screen is described by page objects: plain +classes that read the app and own nothing, so they need no teardown. A page for part of the window, such as a tab, can +derive from the main window's page and so reach its controls too (see `examples/WinUia.Examples.Winforms.UiTests`): ```csharp -public sealed class MainPage : App +using WinUia; +using WinUia.Models; // AppLaunchOptions + +public sealed class MyApp : App { protected override string ExecutablePath => @"C:\path\to\MyApp.exe"; protected override AppLaunchOptions DefaultOptions => new() { ShowPointer = true }; +} - public Element SaveButton => MainWindow.FindByAutomationId("btnSave"); - public SaveDialog Save() { SaveButton.Click(); return new SaveDialog(FindWindow("Save changes?")); } +public class MainForm(MyApp app) +{ + public Element Window => app.MainWindow; + public Element SaveButton => Window.FindByAutomationId("btnSave"); + public SettingsTab OpenSettings() { Window.FindByAutomationId("tabSettings").Select(); return new SettingsTab(app); } +} + +public class SettingsTab(MyApp app) : MainForm(app) // has SaveButton and OpenSettings() as well +{ + public Element Content => Window.FindByAutomationId("lblSettings"); } -using var page = App.Launch(); // its executable and defaults -using var quiet = App.Launch(new AppLaunchOptions { ShowPointer = false }); // one default overridden -using var other = App.Launch(@"D:\builds\MyApp.exe"); // another executable -// Disposing a page closes its app. +using var app = App.Launch(); // its executable and defaults +using var quiet = App.Launch(options: new AppLaunchOptions { ShowPointer = false }); // one default overridden +using var other = App.Launch(@"D:\builds\MyApp.exe"); // another executable + +var mainForm = new MainForm(app); +mainForm.SaveButton.Click(); +var settings = mainForm.OpenSettings(); +// Disposing the app closes it; the pages need nothing. ``` +In NUnit tests, put the launch and the dispose in one base fixture so each test class only writes its own setup +(`examples/WinUia.Examples.Winforms.UiTests/Fixtures/WinFormsFixture.cs`). + ### 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 | `App` (launch, attach, find, close), `AppProcessException`; internal launchers (`WinUia.Launchers`) | `WinUia.Core` (public API only) | -| `WinUia.Core` | Automation | `AutomationContext`, `Element` (lambda searches such as `Find(e => e.Name == "OK")`, self-healing locators), `Poll`, `WinUia.Core.Patterns`, `WinUia.Core.Exceptions`, the UIA COM interop | `WinUia.Input` (public API only) | -| `WinUia.Input` | Platform | `IInputSimulator` and `Win32InputSimulator` (`SendInput`; each `AutomationContext` has one as `Input`, replaceable in tests), `PhysicalDpi` | — | +| `WinUia` | Application | `App` (launch, attach, find, close), `AppLaunchOptions`, `AppProcessException`; internal launchers | `WinUia.Core` (public API only) | +| `WinUia.Core` | Automation | `AutomationContext`, `Element` (lambda searches such as `Find(e => e.Name == "OK")`, self-healing locators), `WinUia.Patterns`, the `Uia*Exception` types, physical input (`WinUia.Input`: `IInputSimulator` and `Win32InputSimulator` via `SendInput`; each `AutomationContext` has one as `Input`, replaceable in tests), the UIA COM and Win32 interop | — | | `WinUia.NUnit` | Test integration | `[UiTest]`: one desktop per test, across test processes; `Eventually(...)` for asynchronous UI state | NUnit, `WinUia.Core` | Module rules: -* Each module's `Interop/` folder is private to that module. Other modules use what it offers (`IInputSimulator`, `PhysicalDpi`), never its P/Invoke or COM declarations. +* Each module's `Interop/` folder is private to that module. Other modules use what it offers (`IInputSimulator`), never its P/Invoke or COM declarations. * Modules use each other'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 test projects. -* Namespaces follow project and folder. One type per file. +* Folders group files by role (for example models, enumerations, exceptions, patterns, interop); a folder exists only when it has files, and test projects mirror the folders of what they test. Namespaces do not follow folders: public and internal types alike use `WinUia`, `WinUia.Patterns`, `WinUia.Input` and `WinUia.NUnit` (visibility is the `internal` keyword's job), except `Interop/`, which keeps its project's `.Interop` namespace as a fence; tests follow project and folder. One type per file. * Package versions are set once, in `Directory.Packages.props`. +* Every public member is listed in the package's `PublicAPI.Unshipped.txt` (moved to `PublicAPI.Shipped.txt` at release); the build fails on an unlisted change, so a public API change always shows in review. Each module has up to three test projects under `tests/`, by what the tests need: @@ -175,7 +202,7 @@ from `tests/`. Examples and tests are independent: neither references the other. ### 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. +* **MTA only:** UI Automation must be called from an MTA thread. Creating an `AutomationContext` (or launching an `App`) 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. diff --git a/.github/actions/setup/action.yaml b/.github/actions/setup/action.yaml new file mode 100644 index 0000000..c1fccf3 --- /dev/null +++ b/.github/actions/setup/action.yaml @@ -0,0 +1,23 @@ +name: Setup +description: Installs the .NET SDK, loads the NuGet cache and restores the solution. Run after checkout. + +runs: + using: composite + steps: + - name: Setup .NET SDK + uses: actions/setup-dotnet@v6 + with: + dotnet-version: '10.0.x' + + - name: Load nuget cache + uses: actions/cache@v6 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/Directory.Packages.props', '**/*.csproj', '**/Directory.Build.props') }} + restore-keys: | + ${{ runner.os }}-nuget- + + # dotnet test needs the restored project assets, even when the binaries come from the build job. + - name: Install dependencies + shell: bash + run: dotnet restore diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml index 62981bb..e5a883d 100644 --- a/.github/workflows/build-and-test.yaml +++ b/.github/workflows/build-and-test.yaml @@ -5,13 +5,10 @@ on: workflow_dispatch: workflow_call: {} -# One run per pull request: a new commit cancels the run still in progress for the previous one. concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true -# WinUia is Windows-only (UI Automation COM, SendInput), so every job runs on Windows. -# build compiles once; unit-test and ui-test run in parallel on its output. jobs: build: name: Build @@ -20,21 +17,11 @@ jobs: - name: Checkout uses: actions/checkout@v7 - - name: Setup .NET SDK - uses: actions/setup-dotnet@v6 - with: - dotnet-version: '10.0.x' - - - name: Load nuget cache - uses: actions/cache@v6 - with: - path: ~/.nuget/packages - key: ${{ runner.os }}-nuget-${{ hashFiles('**/Directory.Packages.props', '**/*.csproj', '**/Directory.Build.props') }} - restore-keys: | - ${{ runner.os }}-nuget- + - name: Setup + uses: ./.github/actions/setup - - name: Install dependencies - run: dotnet restore + - name: Check for unused usings + run: dotnet format style WinUia.slnx --diagnostics IDE0005 --severity info --verify-no-changes - name: Build run: dotnet build --no-restore --configuration Release @@ -59,22 +46,8 @@ jobs: - name: Checkout uses: actions/checkout@v7 - - name: Setup .NET SDK - uses: actions/setup-dotnet@v6 - with: - dotnet-version: '10.0.x' - - - name: Load nuget cache - uses: actions/cache@v6 - with: - path: ~/.nuget/packages - key: ${{ runner.os }}-nuget-${{ hashFiles('**/Directory.Packages.props', '**/*.csproj', '**/Directory.Build.props') }} - restore-keys: | - ${{ runner.os }}-nuget- - - # dotnet test needs the restored project assets to find the test projects; the binaries come from build. - - name: Install dependencies - run: dotnet restore + - name: Setup + uses: ./.github/actions/setup - name: Download build output uses: actions/download-artifact@v8 @@ -86,8 +59,6 @@ jobs: ui-test: name: Run UI tests - # UI tests ([UiTest], category UI) launch apps and drive them through UI Automation and SendInput, so they need an - # interactive desktop session, which GitHub-hosted Windows runners have. needs: build runs-on: windows-latest timeout-minutes: 20 @@ -95,22 +66,8 @@ jobs: - name: Checkout uses: actions/checkout@v7 - - name: Setup .NET SDK - uses: actions/setup-dotnet@v6 - with: - dotnet-version: '10.0.x' - - - name: Load nuget cache - uses: actions/cache@v6 - with: - path: ~/.nuget/packages - key: ${{ runner.os }}-nuget-${{ hashFiles('**/Directory.Packages.props', '**/*.csproj', '**/Directory.Build.props') }} - restore-keys: | - ${{ runner.os }}-nuget- - - # dotnet test needs the restored project assets to find the test projects; the binaries come from build. - - name: Install dependencies - run: dotnet restore + - name: Setup + uses: ./.github/actions/setup - name: Download build output uses: actions/download-artifact@v8 diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 94586f1..8e9338a 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -1,12 +1,8 @@ name: Publish -# Publishing a GitHub release publishes the WinUia packages to GitHub Packages and nuget.org, versioned from the -# release tag: v1.2.0 or V1.2.0 -> 1.2.0, v1.3.0-beta.1 -> 1.3.0-beta.1. -# Authenticates with NuGet trusted publishing: no long-lived API key is stored anywhere. nuget.org trusts this workflow -# (owner buning-software, repository win-uia, workflow publish.yaml, environment nuget) and exchanges its GitHub OIDC -# token for a short-lived API key. The nuget environment holds one secret, NUGET_USER: the nuget.org account name. -# GitHub Packages is pushed to with the workflow's own GITHUB_TOKEN; it links each package to this repository through -# its RepositoryUrl, so they show under the repository's Packages. +# nuget.org trusted publishing: it trusts this workflow (owner buning-software, repository win-uia, workflow +# publish.yaml, environment nuget) and exchanges its OIDC token for a short-lived API key; no API key is stored. +# The nuget environment's NUGET_USER secret is the nuget.org account name. on: release: types: [published] @@ -15,7 +11,6 @@ permissions: contents: read jobs: - # The same build and tests as a pull request; nothing is published unless they pass. test: name: Build and test uses: ./.github/workflows/build-and-test.yaml @@ -27,9 +22,8 @@ jobs: environment: nuget permissions: contents: read - # Lets the GITHUB_TOKEN push to GitHub Packages. packages: write - # Lets NuGet/login request the GitHub OIDC token that nuget.org exchanges for a temporary API key. + # For NuGet/login to request the OIDC token. id-token: write steps: - name: Checkout @@ -53,7 +47,6 @@ jobs: fi echo "version=$version" >> "$GITHUB_OUTPUT" - # Packs the packable projects only: the modules under src/ (tests and examples are not packable). - name: Pack run: >- dotnet pack WinUia.slnx --configuration Release --output packages @@ -66,7 +59,6 @@ jobs: path: packages if-no-files-found: error - # GitHub Packages has no symbol server, so only the .nupkg files go there. - name: Push to GitHub Packages shell: bash env: @@ -77,15 +69,13 @@ jobs: --source "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json" --skip-duplicate done - # The temporary API key is valid for an hour, so log in right before the push. - name: Log in to nuget.org id: nuget-login uses: NuGet/login@v1 with: user: ${{ secrets.NUGET_USER }} - # Pushes every .nupkg; nuget.org picks up the .snupkg symbol package next to each one. Bash expands the glob: - # dotnet nuget push on Windows does not expand "packages/*.nupkg" itself and fails with "File does not exist". + # Bash expands the glob: dotnet nuget push on Windows does not and fails with "File does not exist". - name: Push to nuget.org shell: bash env: diff --git a/Directory.Build.props b/Directory.Build.props index cea0c5d..76975c2 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,7 +4,6 @@ enable enable true - true diff --git a/Directory.Packages.props b/Directory.Packages.props index 49055e0..5404ad5 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,11 +1,11 @@ - true + diff --git a/WinUia.slnx b/WinUia.slnx index 0650e97..2a22885 100644 --- a/WinUia.slnx +++ b/WinUia.slnx @@ -3,7 +3,6 @@ - @@ -12,9 +11,6 @@ - - - diff --git a/examples/Directory.Build.props b/examples/Directory.Build.props index 221014c..3b40914 100644 --- a/examples/Directory.Build.props +++ b/examples/Directory.Build.props @@ -1,10 +1,6 @@ - false diff --git a/examples/WinUia.Examples.Winforms.UiTests/Application/Tab1.cs b/examples/WinUia.Examples.Winforms.UiTests/Application/Tab1.cs deleted file mode 100644 index 3212190..0000000 --- a/examples/WinUia.Examples.Winforms.UiTests/Application/Tab1.cs +++ /dev/null @@ -1,9 +0,0 @@ -using WinUia.Core; - -namespace WinUia.Examples.Winforms.UiTests.Application; - -/// The content of the first tab page, found under the main window. -public class Tab1(Element window) : WinFormsApp -{ - public Element Content => window.FindByAutomationId("lblTab1"); -} diff --git a/examples/WinUia.Examples.Winforms.UiTests/Application/Tab2.cs b/examples/WinUia.Examples.Winforms.UiTests/Application/Tab2.cs deleted file mode 100644 index df14aee..0000000 --- a/examples/WinUia.Examples.Winforms.UiTests/Application/Tab2.cs +++ /dev/null @@ -1,9 +0,0 @@ -using WinUia.Core; - -namespace WinUia.Examples.Winforms.UiTests.Application; - -/// The content of the second tab page, found under the main window. -public class Tab2(Element window) : WinFormsApp -{ - public Element Content => window.FindByAutomationId("lblTab2"); -} diff --git a/examples/WinUia.Examples.Winforms.UiTests/Application/DialogForm.cs b/examples/WinUia.Examples.Winforms.UiTests/Application/Views/DialogForm.cs similarity index 84% rename from examples/WinUia.Examples.Winforms.UiTests/Application/DialogForm.cs rename to examples/WinUia.Examples.Winforms.UiTests/Application/Views/DialogForm.cs index f7c2783..eb943d1 100644 --- a/examples/WinUia.Examples.Winforms.UiTests/Application/DialogForm.cs +++ b/examples/WinUia.Examples.Winforms.UiTests/Application/Views/DialogForm.cs @@ -1,6 +1,4 @@ -using WinUia.Core; - -namespace WinUia.Examples.Winforms.UiTests.Application; +namespace WinUia.Examples.Winforms.UiTests.Application.Views; public sealed class DialogForm(Element window) { diff --git a/examples/WinUia.Examples.Winforms.UiTests/Application/Views/MainForm.cs b/examples/WinUia.Examples.Winforms.UiTests/Application/Views/MainForm.cs new file mode 100644 index 0000000..f8c9c14 --- /dev/null +++ b/examples/WinUia.Examples.Winforms.UiTests/Application/Views/MainForm.cs @@ -0,0 +1,38 @@ +namespace WinUia.Examples.Winforms.UiTests.Application.Views; + +public class MainForm(WinFormsApp app) +{ + public Element Window => app.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(app.FindWindow("WinUia Dialog")); + } + + public Element TabHeader(string text) => + TabControl.Find(e => e.Name == text && e.ControlType == ControlType.TabItem, TreeScope.Children); + + public Tab1 SelectTab1() + { + TabHeader("Tab 1").Select(); + return new Tab1(app); + } + + public Tab2 SelectTab2() + { + TabHeader("Tab 2").Select(); + return new Tab2(app); + } +} diff --git a/examples/WinUia.Examples.Winforms.UiTests/Application/Views/Tab1.cs b/examples/WinUia.Examples.Winforms.UiTests/Application/Views/Tab1.cs new file mode 100644 index 0000000..eb7cccc --- /dev/null +++ b/examples/WinUia.Examples.Winforms.UiTests/Application/Views/Tab1.cs @@ -0,0 +1,6 @@ +namespace WinUia.Examples.Winforms.UiTests.Application.Views; + +public class Tab1(WinFormsApp app) : MainForm(app) +{ + public Element Content => Window.FindByAutomationId("lblTab1"); +} diff --git a/examples/WinUia.Examples.Winforms.UiTests/Application/Views/Tab2.cs b/examples/WinUia.Examples.Winforms.UiTests/Application/Views/Tab2.cs new file mode 100644 index 0000000..8f2cb1a --- /dev/null +++ b/examples/WinUia.Examples.Winforms.UiTests/Application/Views/Tab2.cs @@ -0,0 +1,6 @@ +namespace WinUia.Examples.Winforms.UiTests.Application.Views; + +public class Tab2(WinFormsApp app) : MainForm(app) +{ + public Element Content => Window.FindByAutomationId("lblTab2"); +} diff --git a/examples/WinUia.Examples.Winforms.UiTests/Application/WinFormsApp.cs b/examples/WinUia.Examples.Winforms.UiTests/Application/WinFormsApp.cs index d825c86..cb4e1b8 100644 --- a/examples/WinUia.Examples.Winforms.UiTests/Application/WinFormsApp.cs +++ b/examples/WinUia.Examples.Winforms.UiTests/Application/WinFormsApp.cs @@ -1,55 +1,11 @@ using System.Reflection; -using WinUia.Core; +using WinUia.Models; namespace WinUia.Examples.Winforms.UiTests.Application; -/// -/// Page object for the example WinForms app: the itself, with the controls of its main window. -/// It knows its own executable and defaults, so tests launch it with App.Launch<WinFormsApp>(), or -/// App.Launch<WinFormsApp>(new AppLaunchOptions { ShowPointer = false }) where nobody watches. -/// The tab pages and the dialog are page objects of their own, built on the elements they cover. -/// -public class WinFormsApp : App +public sealed class WinFormsApp : App { - /// protected override string ExecutablePath => typeof(WinFormsApp).Assembly.GetCustomAttributes() .Single(a => a.Key == "ApplicationPath").Value!; - - /// The cursor moves to every element a test interacts with, so you can follow a run. protected override AppLaunchOptions DefaultOptions => new() { ShowPointer = true }; - - 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(FindWindow("WinUia Dialog")); - } - - // 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() - { - TabHeader("Tab 1").Select(); - return new Tab1(Window); - } - - public Tab2 SelectTab2() - { - TabHeader("Tab 2").Select(); - return new Tab2(Window); - } -} +} \ No newline at end of file diff --git a/examples/WinUia.Examples.Winforms.UiTests/Fixtures/WinFormsFixture.cs b/examples/WinUia.Examples.Winforms.UiTests/Fixtures/WinFormsFixture.cs new file mode 100644 index 0000000..aab667d --- /dev/null +++ b/examples/WinUia.Examples.Winforms.UiTests/Fixtures/WinFormsFixture.cs @@ -0,0 +1,20 @@ +using WinUia.Examples.Winforms.UiTests.Application; +using WinUia.Examples.Winforms.UiTests.Application.Views; + +namespace WinUia.Examples.Winforms.UiTests.Fixtures; + +public abstract class WinFormsFixture +{ + protected WinFormsApp WinFormsApp { get; private set; } = null!; + protected MainForm MainForm { get; private set; } = null!; + + [SetUp] + public void LaunchApp() + { + WinFormsApp = App.Launch(); + MainForm = new MainForm(WinFormsApp); + } + + [TearDown] + public void CloseApp() => WinFormsApp.Dispose(); +} diff --git a/examples/WinUia.Examples.Winforms.UiTests/Tests/DialogTests.cs b/examples/WinUia.Examples.Winforms.UiTests/Tests/DialogTests.cs index bee2488..29624cb 100644 --- a/examples/WinUia.Examples.Winforms.UiTests/Tests/DialogTests.cs +++ b/examples/WinUia.Examples.Winforms.UiTests/Tests/DialogTests.cs @@ -1,22 +1,16 @@ using System.Diagnostics; -using WinUia.Core.Exceptions; -using WinUia.Examples.Winforms.UiTests.Application; +using WinUia.Examples.Winforms.UiTests.Fixtures; namespace WinUia.Examples.Winforms.UiTests.Tests; -[UiTest] -public sealed class DialogTests +public sealed class DialogTests : WinFormsFixture { - private WinFormsApp _app = null!; - - [SetUp] - public void SetUp() => _app = App.Launch(); [Test] public void Opening_the_dialog_shows_a_modal_window() { var stopwatch = Stopwatch.StartNew(); - var dialog = _app.OpenDialog(); + var dialog = MainForm.OpenDialog(); using (Assert.EnterMultipleScope()) { @@ -30,34 +24,34 @@ public void Opening_the_dialog_shows_a_modal_window() [Test] public void TryFindWindow_returns_null_for_a_window_that_does_not_exist() { - Assert.That(_app.TryFindWindow("No such window", TimeSpan.FromMilliseconds(100)), Is.Null); + Assert.That(WinFormsApp.TryFindWindow("No such window", TimeSpan.FromMilliseconds(100)), Is.Null); } [Test] public void Confirming_the_dialog_reports_the_entered_value() { - var dialog = _app.OpenDialog(); + var dialog = MainForm.OpenDialog(); dialog.InputBox.SetValue("hello"); dialog.OkButton.Click(); - Eventually(() => _app.ResultLabel.Name == "Dialog: OK (hello)"); + Eventually(() => MainForm.ResultLabel.Name == "Dialog: OK (hello)"); } [Test] public void Cancelling_the_dialog_reports_cancel() { - var dialog = _app.OpenDialog(); + var dialog = MainForm.OpenDialog(); dialog.CancelButton.Click(); - Eventually(() => _app.ResultLabel.Name == "Dialog: Cancel"); + Eventually(() => MainForm.ResultLabel.Name == "Dialog: Cancel"); } [Test] public void Closing_the_dialog_removes_it() { - var dialog = _app.OpenDialog(); + var dialog = MainForm.OpenDialog(); dialog.CancelButton.Click(); @@ -70,11 +64,8 @@ public void Closing_the_dialog_removes_it() } catch (UiaStaleElementException) { - return true; // Closed: the window is gone and cannot be found again. + return true; } }); } - - [TearDown] - public void TearDown() => _app.Dispose(); } diff --git a/examples/WinUia.Examples.Winforms.UiTests/Tests/MainFormTests.cs b/examples/WinUia.Examples.Winforms.UiTests/Tests/MainFormTests.cs index f0c9cd8..7e50e10 100644 --- a/examples/WinUia.Examples.Winforms.UiTests/Tests/MainFormTests.cs +++ b/examples/WinUia.Examples.Winforms.UiTests/Tests/MainFormTests.cs @@ -1,31 +1,28 @@ -using WinUia.Core; -using WinUia.Core.Patterns; -using WinUia.Examples.Winforms.UiTests.Application; +using WinUia.Examples.Winforms.UiTests.Fixtures; +using WinUia.Patterns; namespace WinUia.Examples.Winforms.UiTests.Tests; -[UiTest] -public sealed class MainFormTests +public sealed class MainFormTests : WinFormsFixture { - private WinFormsApp _app = null!; [SetUp] - public void SetUp() => _app = App.Launch(); - + public void SelectTab() => MainForm.SelectTab1(); + [Test] public void Launch_returns_the_main_form() { using (Assert.EnterMultipleScope()) { - Assert.That(_app.Window.Name, Is.EqualTo("WinUia Test App")); - Assert.That(_app.Window.ProcessId, Is.EqualTo(_app.Process.Id)); + Assert.That(MainForm.Window.Name, Is.EqualTo("WinUia Test App")); + Assert.That(MainForm.Window.ProcessId, Is.EqualTo(WinFormsApp.Process.Id)); } } [Test] public void Find_falls_back_to_the_name() { - Assert.That(_app.Find("Click me").AutomationId, Is.EqualTo("btnClick")); + Assert.That(WinFormsApp.Find("Click me").AutomationId, Is.EqualTo("btnClick")); } [Test] @@ -33,19 +30,19 @@ public void The_form_starts_in_its_initial_state() { using (Assert.EnterMultipleScope()) { - Assert.That(_app.ResultLabel.Name, Is.EqualTo("Ready")); - Assert.That(_app.InputBox.ValuePattern.Value, Is.Empty); - Assert.That(_app.ToggleCheckBox.TogglePattern.State, Is.EqualTo(ToggleState.Off)); - Assert.That(_app.VolatileButton.Name, Is.EqualTo("Volatile 1")); + 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(_app.ItemsList.FindAll(e => e.ControlType == ControlType.ListItem, TreeScope.Children).Select(i => i.Name), Is.EqualTo(["Alpha", "Beta", "Gamma"])); + 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 = _app.ItemsList.Find(e => e.Name == "Beta"); + var beta = MainForm.ItemsList.Find(e => e.Name == "Beta"); beta.Select(); @@ -55,35 +52,35 @@ public void Selecting_a_list_item_selects_it() [Test] public void Recreate_increments_the_volatile_button_generation() { - _app.RecreateButton.Click(); - Eventually(() => _app.VolatileButton.Name == "Volatile 2"); + MainForm.RecreateButton.Click(); + Eventually(() => MainForm.VolatileButton.Name == "Volatile 2"); - _app.RecreateButton.Click(); - Eventually(() => _app.VolatileButton.Name == "Volatile 3"); + MainForm.RecreateButton.Click(); + Eventually(() => MainForm.VolatileButton.Name == "Volatile 3"); } [Test] public void Reverse_twice_restores_the_item_order() { - _app.ReverseButton.Click(); + MainForm.ReverseButton.Click(); Eventually(() => ItemNames().SequenceEqual(["Item C", "Item B", "Item A"])); - _app.ReverseButton.Click(); + MainForm.ReverseButton.Click(); Eventually(() => ItemNames().SequenceEqual(["Item A", "Item B", "Item C"])); } [Test] public void Clicking_the_button_updates_the_label() { - _app.Button.Click(); + MainForm.Button.Click(); - Eventually(() => _app.ResultLabel.Name == "Clicked"); + Eventually(() => MainForm.ResultLabel.Name == "Clicked"); } [Test] public void SetValue_round_trips() { - var input = _app.InputBox; + var input = MainForm.InputBox; input.SetValue("round trip"); @@ -93,15 +90,12 @@ public void SetValue_round_trips() [Test] public void Toggle_changes_the_checkbox() { - var toggle = _app.ToggleCheckBox; + var toggle = MainForm.ToggleCheckBox; toggle.Toggle(); Assert.That(toggle.TogglePattern.State, Is.EqualTo(ToggleState.On)); } - [TearDown] - public void TearDown() => _app.Dispose(); - - private IEnumerable ItemNames() => _app.ItemsPanel.FindAll(e => e.ControlType == ControlType.Button, TreeScope.Children).Select(i => i.Name); + private IEnumerable ItemNames() => MainForm.ItemsPanel.FindAll(e => e.ControlType == ControlType.Button, TreeScope.Children).Select(i => i.Name); } diff --git a/examples/WinUia.Examples.Winforms.UiTests/Tests/PatternTests.cs b/examples/WinUia.Examples.Winforms.UiTests/Tests/PatternTests.cs index d2b10ba..9cbf952 100644 --- a/examples/WinUia.Examples.Winforms.UiTests/Tests/PatternTests.cs +++ b/examples/WinUia.Examples.Winforms.UiTests/Tests/PatternTests.cs @@ -1,32 +1,20 @@ -using WinUia.Core; -using WinUia.Core.Exceptions; -using WinUia.Core.Patterns; -using WinUia.Examples.Winforms.UiTests.Application; +using WinUia.Examples.Winforms.UiTests.Fixtures; +using WinUia.Patterns; namespace WinUia.Examples.Winforms.UiTests.Tests; -/// Control patterns, searches, tree navigation and physical input, exercised on the example app's controls. - -[UiTest] -public sealed class PatternTests +public sealed partial class PatternTests : WinFormsFixture { - private WinFormsApp _app = null!; - - [SetUp] - public void SetUp() => _app = App.Launch(); - - [TearDown] - public void TearDown() => _app.Dispose(); - [System.Runtime.InteropServices.DllImport("user32.dll", EntryPoint = "GetWindowLongPtrW")] - private static extern nint GetWindowLongPtr(nint hwnd, int index); + [System.Runtime.InteropServices.LibraryImport("user32.dll", EntryPoint = "GetWindowLongPtrW")] + private static partial nint GetWindowLongPtr(nint hwnd, int index); private static bool IsTopmostWindow(nint hwnd) => (GetWindowLongPtr(hwnd, -20 /* GWL_EXSTYLE */) & 0x8 /* WS_EX_TOPMOST */) != 0; [Test] public void Unsupported_pattern_throws() { - var label = _app.ResultLabel; + var label = MainForm.ResultLabel; Assert.That(label.InvokePattern.IsSupported, Is.False); Assert.Throws(() => label.InvokePattern.Invoke()); @@ -37,15 +25,15 @@ public void Window_pattern_reports_window_state() { using (Assert.EnterMultipleScope()) { - Assert.That(_app.Window.WindowPattern.IsSupported, Is.True); - Assert.That(_app.Window.WindowPattern.VisualState, Is.EqualTo(WindowVisualState.Normal)); + Assert.That(MainForm.Window.WindowPattern.IsSupported, Is.True); + Assert.That(MainForm.Window.WindowPattern.VisualState, Is.EqualTo(WindowVisualState.Normal)); } } [Test] public void GetText_reads_the_value_of_an_edit() { - var input = _app.InputBox; + var input = MainForm.InputBox; input.SetValue("some text"); Assert.That(input.GetText(), Is.EqualTo("some text")); @@ -54,7 +42,7 @@ public void GetText_reads_the_value_of_an_edit() [Test] public void Clickable_point_is_inside_the_bounding_rectangle() { - var button = _app.Button; + var button = MainForm.Button; var point = button.GetClickablePoint(); var rect = button.BoundingRectangle; @@ -69,9 +57,9 @@ public void Clickable_point_is_inside_the_bounding_rectangle() [Test] public void Composite_conditions_search_correctly() { - var both = _app.Window.Find(e => e.AutomationId == "btnClick" && e.ControlType == ControlType.Button); - var either = _app.Window.Find(e => e.AutomationId == "nope" || e.AutomationId == "chkToggle"); - var notButton = _app.Window.FindAll(e => !(e.ControlType == ControlType.Button), TreeScope.Children); + var both = MainForm.Window.Find(e => e.AutomationId == "btnClick" && e.ControlType == ControlType.Button); + var either = MainForm.Window.Find(e => e.AutomationId == "nope" || e.AutomationId == "chkToggle"); + var notButton = MainForm.Window.FindAll(e => !(e.ControlType == ControlType.Button), TreeScope.Children); using (Assert.EnterMultipleScope()) { @@ -84,9 +72,9 @@ public void Composite_conditions_search_correctly() [Test] public void Predicate_searches_find_the_expected_elements() { - var button = _app.Window.Find(e => e.AutomationId == "btnClick" && e.ControlType == ControlType.Button); - var items = _app.ItemsList.FindAll(e => e.ControlType == ControlType.ListItem && e.Name != "Beta", TreeScope.Children); - var missing = _app.Window.TryFind(e => e.Name == "nope", timeout: TimeSpan.Zero); + var button = MainForm.Window.Find(e => e.AutomationId == "btnClick" && e.ControlType == ControlType.Button); + var items = MainForm.ItemsList.FindAll(e => e.ControlType == ControlType.ListItem && e.Name != "Beta", TreeScope.Children); + var missing = MainForm.Window.TryFind(e => e.Name == "nope", timeout: TimeSpan.Zero); using (Assert.EnterMultipleScope()) { @@ -99,29 +87,29 @@ public void Predicate_searches_find_the_expected_elements() [Test] public void FromPoint_and_GetFocusedElement_return_the_expected_elements() { - var input = _app.InputBox; + var input = MainForm.InputBox; input.Focus(); - Eventually(() => _app.Context.GetFocusedElement().AutomationId == "txtInput", "SetFocus moves keyboard focus"); - Assert.That(_app.Context.FromPoint(_app.Button.GetClickablePoint()).AutomationId, Is.EqualTo("btnClick")); + Eventually(() => WinFormsApp.Context.GetFocusedElement().AutomationId == "txtInput", "SetFocus moves keyboard focus"); + Assert.That(WinFormsApp.Context.FromPoint(MainForm.Button.GetClickablePoint()).AutomationId, Is.EqualTo("btnClick")); } [Test] public void Parent_and_Ancestors_walk_up_to_the_root() { - var button = _app.Button; + var button = MainForm.Button; using (Assert.EnterMultipleScope()) { - Assert.That(button.Parent!.IsSameAs(_app.Window), Is.True); - Assert.That(button.Ancestors().Last().IsSameAs(_app.Context.GetRootElement()), Is.True); + Assert.That(button.Parent!.IsSameAs(MainForm.Window), Is.True); + Assert.That(button.Ancestors().Last().IsSameAs(WinFormsApp.Context.GetRootElement()), Is.True); } } [Test] public void AddToSelection_and_RemoveFromSelection_change_a_multi_select_list() { - var list = _app.ItemsList; + var list = MainForm.ItemsList; var alpha = list.Find(e => e.Name == "Alpha"); var gamma = list.Find(e => e.Name == "Gamma"); @@ -144,15 +132,14 @@ public void AddToSelection_and_RemoveFromSelection_change_a_multi_select_list() [Test] public void Window_pattern_members_work() { - var window = _app.Window.WindowPattern; + var window = MainForm.Window.WindowPattern; using (Assert.EnterMultipleScope()) { Assert.That(window.CanMaximize, Is.True); Assert.That(window.CanMinimize, Is.True); Assert.That(window.IsModal, Is.False); - // Compare with Win32 rather than assume the fixture's TopMost: it is the vtable slot under test here. - Assert.That(window.IsTopmost, Is.EqualTo(IsTopmostWindow(_app.Window.NativeWindowHandle))); + Assert.That(window.IsTopmost, Is.EqualTo(IsTopmostWindow(MainForm.Window.NativeWindowHandle))); } // The WinForms provider returns E_NOTIMPL for WaitForInputIdle. @@ -167,42 +154,41 @@ public void Window_pattern_members_work() [Test] public void With_ShowPointer_the_cursor_moves_to_each_element_before_the_interaction() { - _app.Context.ShowPointer = true; - _app.Context.Input.MoveTo(5, 5); + WinFormsApp.Context.ShowPointer = true; + WinFormsApp.Context.Input.MoveTo(5, 5); - _app.Button.Click(); - var afterClick = _app.Context.Input.GetCursorPosition(); - _app.InputBox.SetValue("pointed at"); - var afterSetValue = _app.Context.Input.GetCursorPosition(); + MainForm.Button.Click(); + var afterClick = WinFormsApp.Context.Input.GetCursorPosition(); + MainForm.InputBox.SetValue("pointed at"); + var afterSetValue = WinFormsApp.Context.Input.GetCursorPosition(); using (Assert.EnterMultipleScope()) { - Assert.That(Contains(_app.Button.BoundingRectangle, afterClick), Is.True, "the cursor is on the button after Click"); - Assert.That(Contains(_app.InputBox.BoundingRectangle, afterSetValue), Is.True, "the cursor is on the text box after SetValue"); - Assert.That(_app.InputBox.ValuePattern.Value, Is.EqualTo("pointed at")); + Assert.That(Contains(MainForm.Button.BoundingRectangle, afterClick), Is.True, "the cursor is on the button after Click"); + Assert.That(Contains(MainForm.InputBox.BoundingRectangle, afterSetValue), Is.True, "the cursor is on the text box after SetValue"); + Assert.That(MainForm.InputBox.ValuePattern.Value, Is.EqualTo("pointed at")); } - Eventually(() => _app.ResultLabel.Name == "Clicked", "the click still goes through"); + Eventually(() => MainForm.ResultLabel.Name == "Clicked", "the click still goes through"); } [Test] public void Without_ShowPointer_interactions_do_not_move_the_cursor_to_the_elements() { - _app.Context.ShowPointer = false; // WinFormsApp turns it on by default. - _app.Context.Input.MoveTo(5, 5); // Away from the app's window. + WinFormsApp.Context.ShowPointer = false; + WinFormsApp.Context.Input.MoveTo(5, 5); - _app.Button.Click(); - var afterClick = _app.Context.Input.GetCursorPosition(); - _app.InputBox.SetValue("not pointed at"); - var afterSetValue = _app.Context.Input.GetCursorPosition(); + MainForm.Button.Click(); + var afterClick = WinFormsApp.Context.Input.GetCursorPosition(); + MainForm.InputBox.SetValue("not pointed at"); + var afterSetValue = WinFormsApp.Context.Input.GetCursorPosition(); - // Not "the cursor stays at (5, 5)": someone using the mouse during the run would break that. What ShowPointer - // controls is whether WinUia moves the cursor onto the elements, and nobody else does that by accident. + // Asserts the cursor is not on the elements rather than that it stays at (5, 5), which user mouse movement would break. using (Assert.EnterMultipleScope()) { - Assert.That(Contains(_app.Button.BoundingRectangle, afterClick), Is.False, "Click does not point at the button"); - Assert.That(Contains(_app.InputBox.BoundingRectangle, afterSetValue), Is.False, "SetValue does not point at the text box"); - Assert.That(_app.InputBox.ValuePattern.Value, Is.EqualTo("not pointed at")); + Assert.That(Contains(MainForm.Button.BoundingRectangle, afterClick), Is.False, "Click does not point at the button"); + Assert.That(Contains(MainForm.InputBox.BoundingRectangle, afterSetValue), Is.False, "SetValue does not point at the text box"); + Assert.That(MainForm.InputBox.ValuePattern.Value, Is.EqualTo("not pointed at")); } } @@ -212,12 +198,12 @@ private static bool Contains(ScreenRect rect, (int X, int Y) point) => [Test] public void Physical_click_and_typing_reach_the_app() { - var input = _app.InputBox; + var input = MainForm.InputBox; input.PhysicalClick(); Eventually(() => input.HasKeyboardFocus, "a physical click focuses the text box"); - _app.Context.Input.SendText("typed"); + WinFormsApp.Context.Input.SendText("typed"); Eventually(() => input.ValuePattern.Value == "typed", "SendText types into the focused text box"); } } diff --git a/examples/WinUia.Examples.Winforms.UiTests/Tests/SelfHealingTests.cs b/examples/WinUia.Examples.Winforms.UiTests/Tests/SelfHealingTests.cs index 04f3c81..dfca148 100644 --- a/examples/WinUia.Examples.Winforms.UiTests/Tests/SelfHealingTests.cs +++ b/examples/WinUia.Examples.Winforms.UiTests/Tests/SelfHealingTests.cs @@ -1,39 +1,23 @@ -using WinUia.Core; -using WinUia.Core.Exceptions; -using WinUia.Examples.Winforms.UiTests.Application; +using WinUia.Examples.Winforms.UiTests.Fixtures; namespace WinUia.Examples.Winforms.UiTests.Tests; -/// -/// Elements re-find themselves when their control is re-created: FindFirst results through their search, FindAll -/// results by identity. The example app's "Recreate" button replaces btnVolatile; "Reverse" re-creates the pnlItems buttons -/// in reverse order. -/// -[UiTest] -public sealed class SelfHealingTests +public sealed class SelfHealingTests : WinFormsFixture { - private WinFormsApp _app = null!; - - [SetUp] - public void SetUp() => _app = App.Launch(); - - [TearDown] - public void TearDown() => _app.Dispose(); - - private IReadOnlyList Items() => _app.ItemsPanel.FindAll(e => e.ControlType == ControlType.Button, TreeScope.Children); + private IReadOnlyList Items() => MainForm.ItemsPanel.FindAll(e => e.ControlType == ControlType.Button, TreeScope.Children); private IEnumerable ItemNames() => Items().Select(i => i.Name); private void Recreate() { - _app.RecreateButton.Click(); - Eventually(() => _app.VolatileButton.Name == "Volatile 2", "Recreate replaces the volatile button"); + MainForm.RecreateButton.Click(); + Eventually(() => MainForm.VolatileButton.Name == "Volatile 2", "Recreate replaces the volatile button"); } [Test] public void A_found_element_re_resolves_after_its_control_is_recreated() { - var volatileButton = _app.VolatileButton; + var volatileButton = MainForm.VolatileButton; Assert.That(volatileButton.Name, Is.EqualTo("Volatile 1")); Recreate(); @@ -48,7 +32,7 @@ public void FindAll_elements_re_resolve_to_the_same_control_when_siblings_move() var namesBefore = items.Select(i => i.Name).ToArray(); Assert.That(namesBefore, Is.EquivalentTo(["Item A", "Item B", "Item C"])); - _app.ReverseButton.Click(); + MainForm.ReverseButton.Click(); Eventually(() => ItemNames().SequenceEqual(namesBefore.Reverse())); Assert.That(items.Select(i => i.Name), Is.EqualTo(namesBefore)); @@ -59,7 +43,7 @@ public void A_FindAll_element_never_read_before_going_stale_reports_stale_instea { var items = Items(); - _app.ReverseButton.Click(); + MainForm.ReverseButton.Click(); Eventually(() => ItemNames().First() == "Item C"); Assert.Throws(() => _ = items[0].Name); @@ -68,11 +52,11 @@ public void A_FindAll_element_never_read_before_going_stale_reports_stale_instea [Test] public void Without_stale_retries_a_recreated_control_reports_stale() { - var volatileButton = _app.VolatileButton; + var volatileButton = MainForm.VolatileButton; Assert.That(volatileButton.Name, Is.EqualTo("Volatile 1")); Recreate(); - _app.Context.StaleRetryCount = 0; + WinFormsApp.Context.StaleRetryCount = 0; Assert.Throws(() => _ = volatileButton.Name); } @@ -80,7 +64,7 @@ public void Without_stale_retries_a_recreated_control_reports_stale() [Test] public void An_element_without_a_locator_reports_stale_after_its_control_is_recreated() { - var byHandle = _app.Context.FromHandle(_app.VolatileButton.NativeWindowHandle); + var byHandle = WinFormsApp.Context.FromHandle(MainForm.VolatileButton.NativeWindowHandle); Assert.That(byHandle.Name, Is.EqualTo("Volatile 1")); Recreate(); @@ -91,11 +75,11 @@ public void An_element_without_a_locator_reports_stale_after_its_control_is_recr [Test] public void A_found_child_re_resolves_after_it_and_its_siblings_are_recreated() { - var panel = _app.ItemsPanel; + var panel = MainForm.ItemsPanel; var itemA = panel.Find(e => e.Name == "Item A", TreeScope.Children); Assert.That(itemA.Name, Is.EqualTo("Item A")); - _app.ReverseButton.Click(); + MainForm.ReverseButton.Click(); Eventually(() => ItemNames().First() == "Item C"); Assert.That(itemA.Name, Is.EqualTo("Item A")); diff --git a/examples/WinUia.Examples.Winforms.UiTests/Tests/TabTests.cs b/examples/WinUia.Examples.Winforms.UiTests/Tests/TabTests.cs index 1f8fd2f..32d1999 100644 --- a/examples/WinUia.Examples.Winforms.UiTests/Tests/TabTests.cs +++ b/examples/WinUia.Examples.Winforms.UiTests/Tests/TabTests.cs @@ -1,29 +1,25 @@ using WinUia.Examples.Winforms.UiTests.Application; +using WinUia.Examples.Winforms.UiTests.Fixtures; namespace WinUia.Examples.Winforms.UiTests.Tests; -[UiTest] -public sealed class TabTests +public sealed class TabTests : WinFormsFixture { - private WinFormsApp _app = null!; - - [SetUp] - public void SetUp() => _app = App.Launch(); [Test] public void Tab_1_is_selected_at_start() { using (Assert.EnterMultipleScope()) { - Assert.That(_app.TabHeader("Tab 1").SelectionItemPattern.IsSelected, Is.True); - Assert.That(_app.TabHeader("Tab 2").SelectionItemPattern.IsSelected, Is.False); + 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 = _app.SelectTab1(); + var tab1 = MainForm.SelectTab1(); Assert.That(tab1.Content.Name, Is.EqualTo("tab1 content")); } @@ -31,11 +27,11 @@ public void SelectTab1_shows_the_tab_1_content() [Test] public void SelectTab2_selects_the_tab_and_shows_the_tab_2_content() { - var tab2 = _app.SelectTab2(); + var tab2 = MainForm.SelectTab2(); using (Assert.EnterMultipleScope()) { - Assert.That(_app.TabHeader("Tab 2").SelectionItemPattern.IsSelected, Is.True); + Assert.That(MainForm.TabHeader("Tab 2").SelectionItemPattern.IsSelected, Is.True); Assert.That(tab2.Content.Name, Is.EqualTo("tab2 content")); } } @@ -43,30 +39,34 @@ public void SelectTab2_selects_the_tab_and_shows_the_tab_2_content() [Test] public void Switching_tabs_hides_the_other_tabs_content() { - _app.SelectTab2(); - var tab1 = _app.SelectTab1(); + MainForm.SelectTab2(); + var tab1 = MainForm.SelectTab1(); using (Assert.EnterMultipleScope()) { Assert.That(tab1.Content.Name, Is.EqualTo("tab1 content")); - Assert.That(_app.Window.TryFindByAutomationId("lblTab2", TimeSpan.FromMilliseconds(200)), Is.Null); + Assert.That(MainForm.Window.TryFindByAutomationId("lblTab2", TimeSpan.FromMilliseconds(200)), Is.Null); } } [Test] public void The_main_form_controls_keep_working_while_another_tab_is_selected() { - var tab2 = _app.SelectTab2(); - tab2.SelectTab1(); - _app.Button.Click(); + var tab2 = MainForm.SelectTab2(); + MainForm.Button.Click(); using (Assert.EnterMultipleScope()) { - Eventually(() => _app.ResultLabel.Name == "Clicked"); + Eventually(() => MainForm.ResultLabel.Name == "Clicked"); Assert.That(tab2.Content.Name, Is.EqualTo("tab2 content")); } } - [TearDown] - public void TearDown() => _app.Dispose(); + [Test] + public void A_tab_can_select_another_tab() + { + var tab1 = MainForm.SelectTab2().SelectTab1(); + + Assert.That(tab1.Content.Name, Is.EqualTo("tab1 content")); + } } diff --git a/examples/WinUia.Examples.Winforms.UiTests/Tests/WinFormsAppTests.cs b/examples/WinUia.Examples.Winforms.UiTests/Tests/WinFormsAppTests.cs new file mode 100644 index 0000000..44e8c4c --- /dev/null +++ b/examples/WinUia.Examples.Winforms.UiTests/Tests/WinFormsAppTests.cs @@ -0,0 +1,14 @@ +using WinUia.Examples.Winforms.UiTests.Application; + +namespace WinUia.Examples.Winforms.UiTests.Tests; + +[UiTest] +public class WinFormsAppTests +{ + [Test] + public void Application_launches_and_closes_successfully() + { + using var app = App.Launch(); + app.Close(); + } +} \ No newline at end of file diff --git a/examples/WinUia.Examples.Winforms.UiTests/WinUia.Examples.Winforms.UiTests.csproj b/examples/WinUia.Examples.Winforms.UiTests/WinUia.Examples.Winforms.UiTests.csproj index 82a6f80..bae0d2d 100644 --- a/examples/WinUia.Examples.Winforms.UiTests/WinUia.Examples.Winforms.UiTests.csproj +++ b/examples/WinUia.Examples.Winforms.UiTests/WinUia.Examples.Winforms.UiTests.csproj @@ -1,9 +1,9 @@ - + + + true + @@ -17,12 +17,9 @@ - - - <_Parameter1>ApplicationPath @@ -31,11 +28,16 @@ - + + + <_Parameter1>System.Threading.ApartmentState.MTA <_Parameter1_IsLiteral>true + + + diff --git a/examples/WinUia.Examples.Winforms/DialogForm.cs b/examples/WinUia.Examples.Winforms/DialogForm.cs index 899ec29..2821459 100644 --- a/examples/WinUia.Examples.Winforms/DialogForm.cs +++ b/examples/WinUia.Examples.Winforms/DialogForm.cs @@ -1,6 +1,5 @@ namespace WinUia.Examples.Winforms; -/// Modal dialog opened by btnOpenDialog: asks for a value, closed with OK or Cancel. public partial class DialogForm : Form { public DialogForm() @@ -8,6 +7,5 @@ public DialogForm() InitializeComponent(); } - /// The value typed into the dialog. public string InputText => txtDialogInput.Text; } diff --git a/examples/WinUia.Examples.Winforms/MainForm.cs b/examples/WinUia.Examples.Winforms/MainForm.cs index 02ffdcb..9052943 100644 --- a/examples/WinUia.Examples.Winforms/MainForm.cs +++ b/examples/WinUia.Examples.Winforms/MainForm.cs @@ -1,10 +1,5 @@ namespace WinUia.Examples.Winforms; -/// -/// Main window of the WinForms example app, automated by examples/WinUia.Examples.Winforms.UiTests. 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; @@ -17,7 +12,6 @@ public MainForm() // ReSharper disable once LocalizableElement (the tests read this text) 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(); @@ -25,10 +19,6 @@ private void btnOpenDialog_Click(object? sender, EventArgs e) 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; @@ -38,10 +28,7 @@ private void btnRecreate_Click(object? sender, EventArgs e) 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. - /// + // Reversed with new window handles, so index-based re-resolution would pick the wrong button. private void btnReverse_Click(object? sender, EventArgs e) { var old = pnlItems.Controls.Cast