diff --git a/.github/README.md b/.github/README.md
index f808c70..042ae7e 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
@@ -32,6 +32,7 @@
Getting Started
@@ -65,14 +66,21 @@ 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
- git clone https://github.com/BuningSoftware/win-uia.git
+ git clone https://github.com/buning-software/win-uia.git
cd win-uia
```
@@ -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.Tests`):
+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, and `AppLaunchOptions`, which lives in `WinUia.Models`; 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:
@@ -167,15 +194,15 @@ 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
-* **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.
@@ -206,13 +233,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/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 ddf8693..0ba7108 100644
--- a/.github/workflows/publish.yaml
+++ b/.github/workflows/publish.yaml
@@ -1,7 +1,8 @@
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.
+# 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]
@@ -10,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
@@ -20,14 +20,17 @@ jobs:
needs: test
runs-on: windows-latest
environment: nuget
+ permissions:
+ contents: read
+ packages: write
+ # For NuGet/login to request the OIDC token.
+ id-token: write
steps:
- name: Checkout
uses: actions/checkout@v7
- - name: Setup .NET SDK
- uses: actions/setup-dotnet@v6
- with:
- dotnet-version: '10.0.x'
+ - name: Setup
+ uses: ./.github/actions/setup
- name: Version from the release tag
id: version
@@ -35,14 +38,13 @@ 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
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
@@ -55,11 +57,29 @@ jobs:
path: packages
if-no-files-found: error
- # Pushes every .nupkg; nuget.org picks up the .snupkg symbol package next to each one.
+ - 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
+
+ - name: Log in to nuget.org
+ id: nuget-login
+ uses: NuGet/login@v1
+ with:
+ user: ${{ secrets.NUGET_USER }}
+
+ # 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
- run: >-
- dotnet nuget push "packages/*.nupkg"
- --api-key "${{ secrets.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
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 320f723..2a22885 100644
--- a/WinUia.slnx
+++ b/WinUia.slnx
@@ -3,7 +3,6 @@
-
@@ -12,9 +11,6 @@
-
-
-
@@ -22,7 +18,7 @@
+
-
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.Tests/Application/Tab1.cs b/examples/WinUia.Examples.Winforms.Tests/Application/Tab1.cs
deleted file mode 100644
index 847eef1..0000000
--- a/examples/WinUia.Examples.Winforms.Tests/Application/Tab1.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using WinUia.Core;
-
-namespace WinUia.Examples.Winforms.Tests.Application;
-
-/// The content of the first tab page, found under the main window.
-public sealed class Tab1(Element window)
-{
- public Element Content => window.FindByAutomationId("lblTab1");
-}
diff --git a/examples/WinUia.Examples.Winforms.Tests/Application/Tab2.cs b/examples/WinUia.Examples.Winforms.Tests/Application/Tab2.cs
deleted file mode 100644
index 43a2bbe..0000000
--- a/examples/WinUia.Examples.Winforms.Tests/Application/Tab2.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using WinUia.Core;
-
-namespace WinUia.Examples.Winforms.Tests.Application;
-
-/// The content of the second tab page, found under the main window.
-public sealed class Tab2(Element window)
-{
- public Element Content => window.FindByAutomationId("lblTab2");
-}
diff --git a/examples/WinUia.Examples.Winforms.Tests/Application/WinFormsApp.cs b/examples/WinUia.Examples.Winforms.Tests/Application/WinFormsApp.cs
deleted file mode 100644
index 390533a..0000000
--- a/examples/WinUia.Examples.Winforms.Tests/Application/WinFormsApp.cs
+++ /dev/null
@@ -1,55 +0,0 @@
-using System.Reflection;
-using WinUia.Core;
-
-namespace WinUia.Examples.Winforms.Tests.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 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);
- }
-}
diff --git a/examples/WinUia.Examples.Winforms.Tests/MainFormTests.cs b/examples/WinUia.Examples.Winforms.Tests/MainFormTests.cs
deleted file mode 100644
index a68b5ce..0000000
--- a/examples/WinUia.Examples.Winforms.Tests/MainFormTests.cs
+++ /dev/null
@@ -1,107 +0,0 @@
-using WinUia.Core;
-using WinUia.Core.Patterns;
-using WinUia.Examples.Winforms.Tests.Application;
-
-namespace WinUia.Examples.Winforms.Tests;
-
-[UiTest]
-public sealed class MainFormTests
-{
- private WinFormsApp _app = null!;
-
- [SetUp]
- public void SetUp() => _app = App.Launch();
-
- [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));
- }
- }
-
- [Test]
- public void Find_falls_back_to_the_name()
- {
- Assert.That(_app.Find("Click me").AutomationId, Is.EqualTo("btnClick"));
- }
-
- [Test]
- 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(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"]));
- }
- }
-
- [Test]
- public void Selecting_a_list_item_selects_it()
- {
- var beta = _app.ItemsList.Find(e => e.Name == "Beta");
-
- beta.Select();
-
- Assert.That(beta.SelectionItemPattern.IsSelected, Is.True);
- }
-
- [Test]
- public void Recreate_increments_the_volatile_button_generation()
- {
- _app.RecreateButton.Click();
- Eventually(() => _app.VolatileButton.Name == "Volatile 2");
-
- _app.RecreateButton.Click();
- Eventually(() => _app.VolatileButton.Name == "Volatile 3");
- }
-
- [Test]
- public void Reverse_twice_restores_the_item_order()
- {
- _app.ReverseButton.Click();
- Eventually(() => ItemNames().SequenceEqual(["Item C", "Item B", "Item A"]));
-
- _app.ReverseButton.Click();
- Eventually(() => ItemNames().SequenceEqual(["Item A", "Item B", "Item C"]));
- }
-
- [Test]
- public void Clicking_the_button_updates_the_label()
- {
- _app.Button.Click();
-
- Eventually(() => _app.ResultLabel.Name == "Clicked");
- }
-
- [Test]
- public void SetValue_round_trips()
- {
- var input = _app.InputBox;
-
- input.SetValue("round trip");
-
- Assert.That(input.ValuePattern.Value, Is.EqualTo("round trip"));
- }
-
- [Test]
- public void Toggle_changes_the_checkbox()
- {
- var toggle = _app.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);
-}
diff --git a/examples/WinUia.Examples.Winforms.Tests/TabTests.cs b/examples/WinUia.Examples.Winforms.Tests/TabTests.cs
deleted file mode 100644
index 13e9891..0000000
--- a/examples/WinUia.Examples.Winforms.Tests/TabTests.cs
+++ /dev/null
@@ -1,72 +0,0 @@
-using WinUia.Examples.Winforms.Tests.Application;
-
-namespace WinUia.Examples.Winforms.Tests;
-
-[UiTest]
-public sealed class TabTests
-{
- 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);
- }
- }
-
- [Test]
- public void SelectTab1_shows_the_tab_1_content()
- {
- var tab1 = _app.SelectTab1();
-
- Assert.That(tab1.Content.Name, Is.EqualTo("tab1 content"));
- }
-
- [Test]
- public void SelectTab2_selects_the_tab_and_shows_the_tab_2_content()
- {
- var tab2 = _app.SelectTab2();
-
- using (Assert.EnterMultipleScope())
- {
- Assert.That(_app.TabHeader("Tab 2").SelectionItemPattern.IsSelected, Is.True);
- Assert.That(tab2.Content.Name, Is.EqualTo("tab2 content"));
- }
- }
-
- [Test]
- public void Switching_tabs_hides_the_other_tabs_content()
- {
- _app.SelectTab2();
- var tab1 = _app.SelectTab1();
-
- using (Assert.EnterMultipleScope())
- {
- Assert.That(tab1.Content.Name, Is.EqualTo("tab1 content"));
- Assert.That(_app.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();
-
- _app.Button.Click();
-
- using (Assert.EnterMultipleScope())
- {
- Eventually(() => _app.ResultLabel.Name == "Clicked");
- Assert.That(tab2.Content.Name, Is.EqualTo("tab2 content"));
- }
- }
-
- [TearDown]
- public void TearDown() => _app.Dispose();
-}
diff --git a/examples/WinUia.Examples.Winforms.Tests/Application/DialogForm.cs b/examples/WinUia.Examples.Winforms.UiTests/Application/Views/DialogForm.cs
similarity index 84%
rename from examples/WinUia.Examples.Winforms.Tests/Application/DialogForm.cs
rename to examples/WinUia.Examples.Winforms.UiTests/Application/Views/DialogForm.cs
index a28e525..eb943d1 100644
--- a/examples/WinUia.Examples.Winforms.Tests/Application/DialogForm.cs
+++ b/examples/WinUia.Examples.Winforms.UiTests/Application/Views/DialogForm.cs
@@ -1,7 +1,4 @@
-using WinUia.Core;
-
-
-namespace WinUia.Examples.Winforms.Tests.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
new file mode 100644
index 0000000..cb4e1b8
--- /dev/null
+++ b/examples/WinUia.Examples.Winforms.UiTests/Application/WinFormsApp.cs
@@ -0,0 +1,11 @@
+using System.Reflection;
+using WinUia.Models;
+
+namespace WinUia.Examples.Winforms.UiTests.Application;
+
+public sealed class WinFormsApp : App
+{
+ protected override string ExecutablePath => typeof(WinFormsApp).Assembly.GetCustomAttributes()
+ .Single(a => a.Key == "ApplicationPath").Value!;
+ protected override AppLaunchOptions DefaultOptions => new() { ShowPointer = true };
+}
\ 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.Tests/DialogTests.cs b/examples/WinUia.Examples.Winforms.UiTests/Tests/DialogTests.cs
similarity index 61%
rename from examples/WinUia.Examples.Winforms.Tests/DialogTests.cs
rename to examples/WinUia.Examples.Winforms.UiTests/Tests/DialogTests.cs
index 6b8a048..29624cb 100644
--- a/examples/WinUia.Examples.Winforms.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.Tests.Application;
+using WinUia.Examples.Winforms.UiTests.Fixtures;
-namespace WinUia.Examples.Winforms.Tests;
+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
new file mode 100644
index 0000000..7e50e10
--- /dev/null
+++ b/examples/WinUia.Examples.Winforms.UiTests/Tests/MainFormTests.cs
@@ -0,0 +1,101 @@
+using WinUia.Examples.Winforms.UiTests.Fixtures;
+using WinUia.Patterns;
+
+namespace WinUia.Examples.Winforms.UiTests.Tests;
+
+public sealed class MainFormTests : WinFormsFixture
+{
+
+ [SetUp]
+ public void SelectTab() => MainForm.SelectTab1();
+
+ [Test]
+ public void Launch_returns_the_main_form()
+ {
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(MainForm.Window.Name, Is.EqualTo("WinUia Test App"));
+ Assert.That(MainForm.Window.ProcessId, Is.EqualTo(WinFormsApp.Process.Id));
+ }
+ }
+
+ [Test]
+ public void Find_falls_back_to_the_name()
+ {
+ Assert.That(WinFormsApp.Find("Click me").AutomationId, Is.EqualTo("btnClick"));
+ }
+
+ [Test]
+ public void The_form_starts_in_its_initial_state()
+ {
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(MainForm.ResultLabel.Name, Is.EqualTo("Ready"));
+ Assert.That(MainForm.InputBox.ValuePattern.Value, Is.Empty);
+ Assert.That(MainForm.ToggleCheckBox.TogglePattern.State, Is.EqualTo(ToggleState.Off));
+ Assert.That(MainForm.VolatileButton.Name, Is.EqualTo("Volatile 1"));
+ Assert.That(ItemNames(), Is.EqualTo(["Item A", "Item B", "Item C"]));
+ Assert.That(MainForm.ItemsList.FindAll(e => e.ControlType == ControlType.ListItem, TreeScope.Children).Select(i => i.Name), Is.EqualTo(["Alpha", "Beta", "Gamma"]));
+ }
+ }
+
+ [Test]
+ public void Selecting_a_list_item_selects_it()
+ {
+ var beta = MainForm.ItemsList.Find(e => e.Name == "Beta");
+
+ beta.Select();
+
+ Assert.That(beta.SelectionItemPattern.IsSelected, Is.True);
+ }
+
+ [Test]
+ public void Recreate_increments_the_volatile_button_generation()
+ {
+ MainForm.RecreateButton.Click();
+ Eventually(() => MainForm.VolatileButton.Name == "Volatile 2");
+
+ MainForm.RecreateButton.Click();
+ Eventually(() => MainForm.VolatileButton.Name == "Volatile 3");
+ }
+
+ [Test]
+ public void Reverse_twice_restores_the_item_order()
+ {
+ MainForm.ReverseButton.Click();
+ Eventually(() => ItemNames().SequenceEqual(["Item C", "Item B", "Item A"]));
+
+ MainForm.ReverseButton.Click();
+ Eventually(() => ItemNames().SequenceEqual(["Item A", "Item B", "Item C"]));
+ }
+
+ [Test]
+ public void Clicking_the_button_updates_the_label()
+ {
+ MainForm.Button.Click();
+
+ Eventually(() => MainForm.ResultLabel.Name == "Clicked");
+ }
+
+ [Test]
+ public void SetValue_round_trips()
+ {
+ var input = MainForm.InputBox;
+
+ input.SetValue("round trip");
+
+ Assert.That(input.ValuePattern.Value, Is.EqualTo("round trip"));
+ }
+
+ [Test]
+ public void Toggle_changes_the_checkbox()
+ {
+ var toggle = MainForm.ToggleCheckBox;
+
+ toggle.Toggle();
+
+ Assert.That(toggle.TogglePattern.State, Is.EqualTo(ToggleState.On));
+ }
+
+ private IEnumerable ItemNames() => MainForm.ItemsPanel.FindAll(e => e.ControlType == ControlType.Button, TreeScope.Children).Select(i => i.Name);
+}
diff --git a/examples/WinUia.Examples.Winforms.Tests/PatternTests.cs b/examples/WinUia.Examples.Winforms.UiTests/Tests/PatternTests.cs
similarity index 52%
rename from examples/WinUia.Examples.Winforms.Tests/PatternTests.cs
rename to examples/WinUia.Examples.Winforms.UiTests/Tests/PatternTests.cs
index eb163d2..9cbf952 100644
--- a/examples/WinUia.Examples.Winforms.Tests/PatternTests.cs
+++ b/examples/WinUia.Examples.Winforms.UiTests/Tests/PatternTests.cs
@@ -1,32 +1,20 @@
-using WinUia.Core.Exceptions;
-using WinUia.Core.Patterns;
-using WinUia.Core;
-using WinUia.Examples.Winforms.Tests.Application;
+using WinUia.Examples.Winforms.UiTests.Fixtures;
+using WinUia.Patterns;
-namespace WinUia.Examples.Winforms.Tests;
+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.Tests/SelfHealingTests.cs b/examples/WinUia.Examples.Winforms.UiTests/Tests/SelfHealingTests.cs
similarity index 60%
rename from examples/WinUia.Examples.Winforms.Tests/SelfHealingTests.cs
rename to examples/WinUia.Examples.Winforms.UiTests/Tests/SelfHealingTests.cs
index 2fb9f46..dfca148 100644
--- a/examples/WinUia.Examples.Winforms.Tests/SelfHealingTests.cs
+++ b/examples/WinUia.Examples.Winforms.UiTests/Tests/SelfHealingTests.cs
@@ -1,39 +1,23 @@
-using WinUia.Core.Exceptions;
-using WinUia.Core;
-using WinUia.Examples.Winforms.Tests.Application;
-
-namespace WinUia.Examples.Winforms.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
-{
- private WinFormsApp _app = null!;
-
- [SetUp]
- public void SetUp() => _app = App.Launch();
+using WinUia.Examples.Winforms.UiTests.Fixtures;
- [TearDown]
- public void TearDown() => _app.Dispose();
+namespace WinUia.Examples.Winforms.UiTests.Tests;
- private IReadOnlyList Items() => _app.ItemsPanel.FindAll(e => e.ControlType == ControlType.Button, TreeScope.Children);
+public sealed class SelfHealingTests : WinFormsFixture
+{
+ 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
new file mode 100644
index 0000000..32d1999
--- /dev/null
+++ b/examples/WinUia.Examples.Winforms.UiTests/Tests/TabTests.cs
@@ -0,0 +1,72 @@
+using WinUia.Examples.Winforms.UiTests.Application;
+using WinUia.Examples.Winforms.UiTests.Fixtures;
+
+namespace WinUia.Examples.Winforms.UiTests.Tests;
+
+public sealed class TabTests : WinFormsFixture
+{
+
+ [Test]
+ public void Tab_1_is_selected_at_start()
+ {
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(MainForm.TabHeader("Tab 1").SelectionItemPattern.IsSelected, Is.True);
+ Assert.That(MainForm.TabHeader("Tab 2").SelectionItemPattern.IsSelected, Is.False);
+ }
+ }
+
+ [Test]
+ public void SelectTab1_shows_the_tab_1_content()
+ {
+ var tab1 = MainForm.SelectTab1();
+
+ Assert.That(tab1.Content.Name, Is.EqualTo("tab1 content"));
+ }
+
+ [Test]
+ public void SelectTab2_selects_the_tab_and_shows_the_tab_2_content()
+ {
+ var tab2 = MainForm.SelectTab2();
+
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(MainForm.TabHeader("Tab 2").SelectionItemPattern.IsSelected, Is.True);
+ Assert.That(tab2.Content.Name, Is.EqualTo("tab2 content"));
+ }
+ }
+
+ [Test]
+ public void Switching_tabs_hides_the_other_tabs_content()
+ {
+ MainForm.SelectTab2();
+ var tab1 = MainForm.SelectTab1();
+
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(tab1.Content.Name, Is.EqualTo("tab1 content"));
+ Assert.That(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 = MainForm.SelectTab2();
+ MainForm.Button.Click();
+
+ using (Assert.EnterMultipleScope())
+ {
+ Eventually(() => MainForm.ResultLabel.Name == "Clicked");
+ Assert.That(tab2.Content.Name, Is.EqualTo("tab2 content"));
+ }
+ }
+
+ [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.Tests/WinUia.Examples.Winforms.Tests.csproj b/examples/WinUia.Examples.Winforms.UiTests/WinUia.Examples.Winforms.UiTests.csproj
similarity index 73%
rename from examples/WinUia.Examples.Winforms.Tests/WinUia.Examples.Winforms.Tests.csproj
rename to examples/WinUia.Examples.Winforms.UiTests/WinUia.Examples.Winforms.UiTests.csproj
index 82a6f80..bae0d2d 100644
--- a/examples/WinUia.Examples.Winforms.Tests/WinUia.Examples.Winforms.Tests.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.Designer.cs b/examples/WinUia.Examples.Winforms/DialogForm.Designer.cs
index 2c35ca5..fe78c23 100644
--- a/examples/WinUia.Examples.Winforms/DialogForm.Designer.cs
+++ b/examples/WinUia.Examples.Winforms/DialogForm.Designer.cs
@@ -90,6 +90,7 @@ private void InitializeComponent()
ShowInTaskbar = false;
StartPosition = FormStartPosition.CenterParent;
Text = "WinUia Dialog";
+ TopMost = true;
ResumeLayout(false);
PerformLayout();
}
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/DialogForm.resx b/examples/WinUia.Examples.Winforms/DialogForm.resx
new file mode 100644
index 0000000..1af7de1
--- /dev/null
+++ b/examples/WinUia.Examples.Winforms/DialogForm.resx
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/examples/WinUia.Examples.Winforms/MainForm.cs b/examples/WinUia.Examples.Winforms/MainForm.cs
index 7c1d6cc..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.Tests. The controls are laid out in the designer; WinForms exposes each
-/// control's as its UI Automation AutomationId, so those names are the ids the tests
-/// search for. The window is TopMost because the physical-input tests click screen coordinates.
-///
public partial class MainForm : Form
{
private int _generation = 1;
@@ -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
diff --git a/tests/WinUia.Core.IntegrationTests/ElementPredicateConditionTests.cs b/tests/WinUia.Core.IntegrationTests/Helpers/ElementPredicateConditionTests.cs
similarity index 72%
rename from tests/WinUia.Core.IntegrationTests/ElementPredicateConditionTests.cs
rename to tests/WinUia.Core.IntegrationTests/Helpers/ElementPredicateConditionTests.cs
index 6144b6e..6a1d926 100644
--- a/tests/WinUia.Core.IntegrationTests/ElementPredicateConditionTests.cs
+++ b/tests/WinUia.Core.IntegrationTests/Helpers/ElementPredicateConditionTests.cs
@@ -1,6 +1,6 @@
-namespace WinUia.Core.IntegrationTests;
-/// Canonical search predicates turned into real UIA condition objects (the expression side is unit-tested).
+namespace WinUia.Core.IntegrationTests.Helpers;
+
public class ElementPredicateConditionTests
{
[Test]
diff --git a/tests/WinUia.Core.IntegrationTests/InputSimulatorTests.cs b/tests/WinUia.Core.IntegrationTests/Input/InputSimulatorTests.cs
similarity index 81%
rename from tests/WinUia.Core.IntegrationTests/InputSimulatorTests.cs
rename to tests/WinUia.Core.IntegrationTests/Input/InputSimulatorTests.cs
index cbdd2e2..1892ede 100644
--- a/tests/WinUia.Core.IntegrationTests/InputSimulatorTests.cs
+++ b/tests/WinUia.Core.IntegrationTests/Input/InputSimulatorTests.cs
@@ -1,9 +1,5 @@
-namespace WinUia.Core.IntegrationTests;
+namespace WinUia.Core.IntegrationTests.Input;
-///
-/// Elements send physical input through their context's , so it can be replaced:
-/// these tests click the real desktop element without the real mouse moving.
-///
public class InputSimulatorTests
{
[Test]
diff --git a/tests/WinUia.Input.IntegrationTests/PhysicalDpiTests.cs b/tests/WinUia.Core.IntegrationTests/Input/PhysicalDpiTests.cs
similarity index 96%
rename from tests/WinUia.Input.IntegrationTests/PhysicalDpiTests.cs
rename to tests/WinUia.Core.IntegrationTests/Input/PhysicalDpiTests.cs
index 31b2c0b..0e62850 100644
--- a/tests/WinUia.Input.IntegrationTests/PhysicalDpiTests.cs
+++ b/tests/WinUia.Core.IntegrationTests/Input/PhysicalDpiTests.cs
@@ -1,6 +1,6 @@
using System.Runtime.InteropServices;
-namespace WinUia.Input.IntegrationTests;
+namespace WinUia.Core.IntegrationTests.Input;
public class PhysicalDpiTests
{
diff --git a/tests/WinUia.Core.IntegrationTests/RecordingInputSimulator.cs b/tests/WinUia.Core.IntegrationTests/Input/RecordingInputSimulator.cs
similarity index 82%
rename from tests/WinUia.Core.IntegrationTests/RecordingInputSimulator.cs
rename to tests/WinUia.Core.IntegrationTests/Input/RecordingInputSimulator.cs
index 472a751..cd509ae 100644
--- a/tests/WinUia.Core.IntegrationTests/RecordingInputSimulator.cs
+++ b/tests/WinUia.Core.IntegrationTests/Input/RecordingInputSimulator.cs
@@ -1,8 +1,7 @@
using WinUia.Input;
-namespace WinUia.Core.IntegrationTests;
+namespace WinUia.Core.IntegrationTests.Input;
-/// An that records what would be sent instead of touching the real mouse and keyboard.
internal sealed class RecordingInputSimulator : IInputSimulator
{
private (int X, int Y) _cursor;
diff --git a/tests/WinUia.Core.IntegrationTests/InteropVtableTests.cs b/tests/WinUia.Core.IntegrationTests/Interop/InteropVtableTests.cs
similarity index 56%
rename from tests/WinUia.Core.IntegrationTests/InteropVtableTests.cs
rename to tests/WinUia.Core.IntegrationTests/Interop/InteropVtableTests.cs
index bb9d302..0793d96 100644
--- a/tests/WinUia.Core.IntegrationTests/InteropVtableTests.cs
+++ b/tests/WinUia.Core.IntegrationTests/Interop/InteropVtableTests.cs
@@ -1,42 +1,38 @@
-namespace WinUia.Core.IntegrationTests;
+namespace WinUia.Core.IntegrationTests.Interop;
-///
-/// Guards the hand-written vtables: each typed getter must agree with the generic
-/// GetCurrentPropertyValue(propertyId). A misaligned slot returns a different property (or crashes).
-///
public class InteropVtableTests
{
private static void AssertTypedGettersMatchPropertyValues(Element element)
{
- var raw = element.Raw;
+ var raw = element.Handle.Raw;
using (Assert.EnterMultipleScope())
{
- Assert.That(raw.CurrentName, Is.EqualTo(raw.GetCurrentPropertyValue(30005) ?? "")); // Name
- Assert.That(raw.CurrentAutomationId, Is.EqualTo(raw.GetCurrentPropertyValue(30011) ?? "")); // AutomationId
- Assert.That(raw.CurrentClassName, Is.EqualTo(raw.GetCurrentPropertyValue(30012) ?? "")); // ClassName
- Assert.That(raw.CurrentFrameworkId, Is.EqualTo(raw.GetCurrentPropertyValue(30024) ?? "")); // FrameworkId
- Assert.That(raw.CurrentLocalizedControlType ?? "", Is.EqualTo(raw.GetCurrentPropertyValue(30004) ?? "")); // LocalizedControlType
- Assert.That(raw.CurrentControlType, Is.EqualTo(raw.GetCurrentPropertyValue(30003))); // ControlType
- Assert.That(raw.CurrentProcessId, Is.EqualTo(raw.GetCurrentPropertyValue(30002))); // ProcessId
- Assert.That(raw.CurrentIsEnabled, Is.EqualTo(raw.GetCurrentPropertyValue(30010))); // IsEnabled
- Assert.That(raw.CurrentIsOffscreen, Is.EqualTo(raw.GetCurrentPropertyValue(30022))); // IsOffscreen
- Assert.That(raw.CurrentHasKeyboardFocus, Is.EqualTo(raw.GetCurrentPropertyValue(30008))); // HasKeyboardFocus
- Assert.That(raw.CurrentIsKeyboardFocusable, Is.EqualTo(raw.GetCurrentPropertyValue(30009))); // IsKeyboardFocusable
- Assert.That(raw.CurrentIsControlElement, Is.EqualTo(raw.GetCurrentPropertyValue(30016))); // IsControlElement
- Assert.That(raw.CurrentIsPassword, Is.EqualTo(raw.GetCurrentPropertyValue(30019))); // IsPassword
- Assert.That((long)raw.CurrentNativeWindowHandle, Is.EqualTo(Convert.ToInt64(raw.GetCurrentPropertyValue(30020)))); // NativeWindowHandle
- Assert.That(raw.CurrentAcceleratorKey ?? "", Is.EqualTo(raw.GetCurrentPropertyValue(30006) ?? "")); // AcceleratorKey
- Assert.That(raw.CurrentAccessKey ?? "", Is.EqualTo(raw.GetCurrentPropertyValue(30007) ?? "")); // AccessKey
- Assert.That(raw.CurrentHelpText ?? "", Is.EqualTo(raw.GetCurrentPropertyValue(30013) ?? "")); // HelpText
- Assert.That(raw.CurrentCulture, Is.EqualTo(raw.GetCurrentPropertyValue(30015))); // Culture
- Assert.That(raw.CurrentIsContentElement, Is.EqualTo(raw.GetCurrentPropertyValue(30017))); // IsContentElement
- Assert.That(raw.CurrentItemType ?? "", Is.EqualTo(raw.GetCurrentPropertyValue(30021) ?? "")); // ItemType
- Assert.That(raw.CurrentOrientation, Is.EqualTo(raw.GetCurrentPropertyValue(30023))); // Orientation
- Assert.That(raw.CurrentIsRequiredForForm, Is.EqualTo(raw.GetCurrentPropertyValue(30025))); // IsRequiredForForm
- Assert.That(raw.CurrentItemStatus ?? "", Is.EqualTo(raw.GetCurrentPropertyValue(30026) ?? "")); // ItemStatus
-
- var bounds = (double[])raw.GetCurrentPropertyValue(30001)!; // BoundingRectangle: l, t, w, h
+ Assert.That(raw.CurrentName, Is.EqualTo(raw.GetCurrentPropertyValue(30005) ?? ""));
+ Assert.That(raw.CurrentAutomationId, Is.EqualTo(raw.GetCurrentPropertyValue(30011) ?? ""));
+ Assert.That(raw.CurrentClassName, Is.EqualTo(raw.GetCurrentPropertyValue(30012) ?? ""));
+ Assert.That(raw.CurrentFrameworkId, Is.EqualTo(raw.GetCurrentPropertyValue(30024) ?? ""));
+ Assert.That(raw.CurrentLocalizedControlType ?? "", Is.EqualTo(raw.GetCurrentPropertyValue(30004) ?? ""));
+ Assert.That(raw.CurrentControlType, Is.EqualTo(raw.GetCurrentPropertyValue(30003)));
+ Assert.That(raw.CurrentProcessId, Is.EqualTo(raw.GetCurrentPropertyValue(30002)));
+ Assert.That(raw.CurrentIsEnabled, Is.EqualTo(raw.GetCurrentPropertyValue(30010)));
+ Assert.That(raw.CurrentIsOffscreen, Is.EqualTo(raw.GetCurrentPropertyValue(30022)));
+ Assert.That(raw.CurrentHasKeyboardFocus, Is.EqualTo(raw.GetCurrentPropertyValue(30008)));
+ Assert.That(raw.CurrentIsKeyboardFocusable, Is.EqualTo(raw.GetCurrentPropertyValue(30009)));
+ Assert.That(raw.CurrentIsControlElement, Is.EqualTo(raw.GetCurrentPropertyValue(30016)));
+ Assert.That(raw.CurrentIsPassword, Is.EqualTo(raw.GetCurrentPropertyValue(30019)));
+ Assert.That((long)raw.CurrentNativeWindowHandle, Is.EqualTo(Convert.ToInt64(raw.GetCurrentPropertyValue(30020))));
+ Assert.That(raw.CurrentAcceleratorKey ?? "", Is.EqualTo(raw.GetCurrentPropertyValue(30006) ?? ""));
+ Assert.That(raw.CurrentAccessKey ?? "", Is.EqualTo(raw.GetCurrentPropertyValue(30007) ?? ""));
+ Assert.That(raw.CurrentHelpText ?? "", Is.EqualTo(raw.GetCurrentPropertyValue(30013) ?? ""));
+ Assert.That(raw.CurrentCulture, Is.EqualTo(raw.GetCurrentPropertyValue(30015)));
+ Assert.That(raw.CurrentIsContentElement, Is.EqualTo(raw.GetCurrentPropertyValue(30017)));
+ Assert.That(raw.CurrentItemType ?? "", Is.EqualTo(raw.GetCurrentPropertyValue(30021) ?? ""));
+ Assert.That(raw.CurrentOrientation, Is.EqualTo(raw.GetCurrentPropertyValue(30023)));
+ Assert.That(raw.CurrentIsRequiredForForm, Is.EqualTo(raw.GetCurrentPropertyValue(30025)));
+ Assert.That(raw.CurrentItemStatus ?? "", Is.EqualTo(raw.GetCurrentPropertyValue(30026) ?? ""));
+
+ var bounds = (double[])raw.GetCurrentPropertyValue(30001)!; // l, t, w, h
var rect = raw.CurrentBoundingRectangle;
Assert.That(rect.left, Is.EqualTo((int)bounds[0]));
Assert.That(rect.top, Is.EqualTo((int)bounds[1]));
diff --git a/tests/WinUia.Core.IntegrationTests/AutomationContextTests.cs b/tests/WinUia.Core.IntegrationTests/Model/AutomationContextTests.cs
similarity index 98%
rename from tests/WinUia.Core.IntegrationTests/AutomationContextTests.cs
rename to tests/WinUia.Core.IntegrationTests/Model/AutomationContextTests.cs
index e8ed2f0..2a60cf5 100644
--- a/tests/WinUia.Core.IntegrationTests/AutomationContextTests.cs
+++ b/tests/WinUia.Core.IntegrationTests/Model/AutomationContextTests.cs
@@ -1,6 +1,6 @@
using System.Diagnostics;
-namespace WinUia.Core.IntegrationTests;
+namespace WinUia.Core.IntegrationTests.Model;
public class AutomationContextTests
{
diff --git a/tests/WinUia.Core.IntegrationTests/WinUia.Core.IntegrationTests.csproj b/tests/WinUia.Core.IntegrationTests/WinUia.Core.IntegrationTests.csproj
index 345cef8..eb707c5 100644
--- a/tests/WinUia.Core.IntegrationTests/WinUia.Core.IntegrationTests.csproj
+++ b/tests/WinUia.Core.IntegrationTests/WinUia.Core.IntegrationTests.csproj
@@ -1,7 +1,6 @@
-
diff --git a/tests/WinUia.Input.UiTests/CursorTests.cs b/tests/WinUia.Core.UiTests/Input/CursorTests.cs
similarity index 92%
rename from tests/WinUia.Input.UiTests/CursorTests.cs
rename to tests/WinUia.Core.UiTests/Input/CursorTests.cs
index d354ef5..1fbe9e3 100644
--- a/tests/WinUia.Input.UiTests/CursorTests.cs
+++ b/tests/WinUia.Core.UiTests/Input/CursorTests.cs
@@ -1,8 +1,8 @@
using System.Diagnostics;
+using WinUia.Input;
-namespace WinUia.Input.UiTests;
+namespace WinUia.Core.UiTests.Input;
-/// Moves the real cursor, so these run as UI tests (one desktop, interactive session).
public class CursorTests
{
private readonly Win32InputSimulator _input = new();
diff --git a/tests/WinUia.Core.UiTests/Model/WindowPropertyTests.cs b/tests/WinUia.Core.UiTests/Model/WindowPropertyTests.cs
new file mode 100644
index 0000000..5768264
--- /dev/null
+++ b/tests/WinUia.Core.UiTests/Model/WindowPropertyTests.cs
@@ -0,0 +1,33 @@
+using WinUia.Testing.Shared;
+
+namespace WinUia.Core.UiTests.Model;
+
+public class WindowPropertyTests
+{
+ [Test]
+ public void A_window_reports_the_same_values_through_typed_properties_and_raw_UIA_properties()
+ {
+ using var app = App.Launch(AppPaths.TestApp);
+ var window = app.MainWindow;
+
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(window.Name, Is.EqualTo(window.Handle.Raw.GetCurrentPropertyValue(30005) ?? ""));
+ Assert.That(window.AutomationId, Is.EqualTo(window.Handle.Raw.GetCurrentPropertyValue(30011) ?? ""));
+ Assert.That(window.ClassName, Is.EqualTo(window.Handle.Raw.GetCurrentPropertyValue(30012) ?? ""));
+ Assert.That(window.FrameworkId, Is.EqualTo(window.Handle.Raw.GetCurrentPropertyValue(30024) ?? ""));
+ Assert.That((int)window.ControlType, Is.EqualTo(window.Handle.Raw.GetCurrentPropertyValue(30003)));
+ Assert.That(window.ProcessId, Is.EqualTo(window.Handle.Raw.GetCurrentPropertyValue(30002)));
+ Assert.That(window.IsEnabled, Is.EqualTo(window.Handle.Raw.GetCurrentPropertyValue(30010)));
+ Assert.That(window.IsOffscreen, Is.EqualTo(window.Handle.Raw.GetCurrentPropertyValue(30022)));
+ Assert.That((long)window.NativeWindowHandle, Is.EqualTo(Convert.ToInt64(window.Handle.Raw.GetCurrentPropertyValue(30020))));
+
+ var bounds = (double[])window.Handle.Raw.GetCurrentPropertyValue(30001)!; // l, t, w, h
+ var rect = window.BoundingRectangle;
+ Assert.That(rect.Left, Is.EqualTo((int)bounds[0]));
+ Assert.That(rect.Top, Is.EqualTo((int)bounds[1]));
+ Assert.That(rect.Right, Is.EqualTo((int)(bounds[0] + bounds[2])));
+ Assert.That(rect.Bottom, Is.EqualTo((int)(bounds[1] + bounds[3])));
+ }
+ }
+}
diff --git a/tests/WinUia.Core.UiTests/WinUia.Core.UiTests.csproj b/tests/WinUia.Core.UiTests/WinUia.Core.UiTests.csproj
index f8e7ff1..27af0e5 100644
--- a/tests/WinUia.Core.UiTests/WinUia.Core.UiTests.csproj
+++ b/tests/WinUia.Core.UiTests/WinUia.Core.UiTests.csproj
@@ -1,11 +1,8 @@
-
-
-
diff --git a/tests/WinUia.Core.UiTests/WindowPropertyTests.cs b/tests/WinUia.Core.UiTests/WindowPropertyTests.cs
deleted file mode 100644
index 63732d5..0000000
--- a/tests/WinUia.Core.UiTests/WindowPropertyTests.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-using WinUia.Testing.Shared;
-
-namespace WinUia.Core.UiTests;
-
-///
-/// A real application window reports the same values through Element's typed properties and through the generic
-/// GetPropertyValue, so the hand-written vtable slots behind them line up for windows too (the desktop root is
-/// covered by the integration tests).
-///
-public class WindowPropertyTests
-{
- [Test]
- public void A_window_reports_the_same_values_through_typed_properties_and_GetPropertyValue()
- {
- using var app = App.Launch(AppPaths.TestApp);
- var window = app.MainWindow;
-
- using (Assert.EnterMultipleScope())
- {
- Assert.That(window.Name, Is.EqualTo(window.GetPropertyValue(30005) ?? "")); // Name
- Assert.That(window.AutomationId, Is.EqualTo(window.GetPropertyValue(30011) ?? "")); // AutomationId
- Assert.That(window.ClassName, Is.EqualTo(window.GetPropertyValue(30012) ?? "")); // ClassName
- Assert.That(window.FrameworkId, Is.EqualTo(window.GetPropertyValue(30024) ?? "")); // FrameworkId
- Assert.That((int)window.ControlType, Is.EqualTo(window.GetPropertyValue(30003))); // ControlType
- Assert.That(window.ProcessId, Is.EqualTo(window.GetPropertyValue(30002))); // ProcessId
- Assert.That(window.IsEnabled, Is.EqualTo(window.GetPropertyValue(30010))); // IsEnabled
- Assert.That(window.IsOffscreen, Is.EqualTo(window.GetPropertyValue(30022))); // IsOffscreen
- Assert.That((long)window.NativeWindowHandle, Is.EqualTo(Convert.ToInt64(window.GetPropertyValue(30020)))); // NativeWindowHandle
-
- var bounds = (double[])window.GetPropertyValue(30001)!; // BoundingRectangle: l, t, w, h
- var rect = window.BoundingRectangle;
- Assert.That(rect.Left, Is.EqualTo((int)bounds[0]));
- Assert.That(rect.Top, Is.EqualTo((int)bounds[1]));
- Assert.That(rect.Right, Is.EqualTo((int)(bounds[0] + bounds[2])));
- Assert.That(rect.Bottom, Is.EqualTo((int)(bounds[1] + bounds[3])));
- }
- }
-}
diff --git a/tests/WinUia.Core.UnitTests/AutomationIdValue.cs b/tests/WinUia.Core.UnitTests/AutomationIdValue.cs
deleted file mode 100644
index f62bc34..0000000
--- a/tests/WinUia.Core.UnitTests/AutomationIdValue.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace WinUia.Core.UnitTests;
-
-/// A typed id with an implicit conversion to string, the way page objects often name their controls.
-internal sealed record AutomationIdValue(string Value)
-{
- public static implicit operator string(AutomationIdValue id) => id.Value;
-}
diff --git a/tests/WinUia.Core.UnitTests/ComExceptionMapperTests.cs b/tests/WinUia.Core.UnitTests/Exceptions/ComExceptionMapperTests.cs
similarity index 90%
rename from tests/WinUia.Core.UnitTests/ComExceptionMapperTests.cs
rename to tests/WinUia.Core.UnitTests/Exceptions/ComExceptionMapperTests.cs
index a337c44..fbd0c47 100644
--- a/tests/WinUia.Core.UnitTests/ComExceptionMapperTests.cs
+++ b/tests/WinUia.Core.UnitTests/Exceptions/ComExceptionMapperTests.cs
@@ -1,7 +1,6 @@
using System.Runtime.InteropServices;
-using WinUia.Core.Exceptions;
-namespace WinUia.Core.UnitTests;
+namespace WinUia.Core.UnitTests.Exceptions;
public class ComExceptionMapperTests
{
@@ -11,8 +10,8 @@ public class ComExceptionMapperTests
new object[] { 0x80010108u, typeof(UiaStaleElementException) }, // RPC_E_DISCONNECTED
new object[] { 0x800706BAu, typeof(UiaStaleElementException) }, // RPC_S_SERVER_UNAVAILABLE
new object[] { 0x800706BEu, typeof(UiaStaleElementException) }, // RPC_S_CALL_FAILED
- new object[] { 0x80040200u, typeof(UiaElementNotEnabledException) }, // UIA_E_ELEMENTNOTENABLED
- new object[] { 0x80040202u, typeof(UiaNoClickablePointException) }, // UIA_E_NOCLICKABLEPOINT
+ new object[] { 0x80040200u, typeof(UiaException) }, // UIA_E_ELEMENTNOTENABLED
+ new object[] { 0x80040202u, typeof(UiaException) }, // UIA_E_NOCLICKABLEPOINT
new object[] { 0x80040204u, typeof(UiaPatternNotSupportedException) },// UIA_E_NOTSUPPORTED
new object[] { 0x80131505u, typeof(UiaTimeoutException) }, // UIA_E_TIMEOUT
new object[] { 0x80004005u, typeof(UiaException) }, // E_FAIL is not "not found"
diff --git a/tests/WinUia.Core.UnitTests/Helpers/AutomationIdValue.cs b/tests/WinUia.Core.UnitTests/Helpers/AutomationIdValue.cs
new file mode 100644
index 0000000..bc888f0
--- /dev/null
+++ b/tests/WinUia.Core.UnitTests/Helpers/AutomationIdValue.cs
@@ -0,0 +1,6 @@
+namespace WinUia.Core.UnitTests.Helpers;
+
+internal sealed record AutomationIdValue(string Value)
+{
+ public static implicit operator string(AutomationIdValue id) => id.Value;
+}
diff --git a/tests/WinUia.Core.UnitTests/ElementPredicateTests.cs b/tests/WinUia.Core.UnitTests/Helpers/ElementPredicateTests.cs
similarity index 98%
rename from tests/WinUia.Core.UnitTests/ElementPredicateTests.cs
rename to tests/WinUia.Core.UnitTests/Helpers/ElementPredicateTests.cs
index 4ea980e..7d5abf6 100644
--- a/tests/WinUia.Core.UnitTests/ElementPredicateTests.cs
+++ b/tests/WinUia.Core.UnitTests/Helpers/ElementPredicateTests.cs
@@ -1,6 +1,6 @@
using System.Linq.Expressions;
-namespace WinUia.Core.UnitTests;
+namespace WinUia.Core.UnitTests.Helpers;
public class ElementPredicateTests
{
diff --git a/tests/WinUia.Core.UnitTests/ProcessHolder.cs b/tests/WinUia.Core.UnitTests/Helpers/ProcessHolder.cs
similarity index 60%
rename from tests/WinUia.Core.UnitTests/ProcessHolder.cs
rename to tests/WinUia.Core.UnitTests/Helpers/ProcessHolder.cs
index 77573f3..459a635 100644
--- a/tests/WinUia.Core.UnitTests/ProcessHolder.cs
+++ b/tests/WinUia.Core.UnitTests/Helpers/ProcessHolder.cs
@@ -1,8 +1,7 @@
using System.Linq.Expressions;
-namespace WinUia.Core.UnitTests;
+namespace WinUia.Core.UnitTests.Helpers;
-/// Stands in for a page object whose search reads one of its own members.
internal sealed class ProcessHolder(int id)
{
public int Id { get; } = id;
diff --git a/tests/WinUia.Core.UnitTests/RetryTests.cs b/tests/WinUia.Core.UnitTests/Helpers/RetryTests.cs
similarity index 92%
rename from tests/WinUia.Core.UnitTests/RetryTests.cs
rename to tests/WinUia.Core.UnitTests/Helpers/RetryTests.cs
index 7bd00d0..81cf208 100644
--- a/tests/WinUia.Core.UnitTests/RetryTests.cs
+++ b/tests/WinUia.Core.UnitTests/Helpers/RetryTests.cs
@@ -1,7 +1,6 @@
using System.Runtime.InteropServices;
-using WinUia.Core.Exceptions;
-namespace WinUia.Core.UnitTests;
+namespace WinUia.Core.UnitTests.Helpers;
public class RetryTests
{
@@ -48,7 +47,7 @@ public void OnStale_does_not_retry_other_failures()
{
var calls = 0;
- Assert.Throws(() =>
+ Assert.Throws(() =>
Retry.OnStale(() => { calls++; throw new COMException("x", unchecked((int)0x80040200)); }, () => true, maxRetries: 3));
Assert.That(calls, Is.EqualTo(1));
}
diff --git a/tests/WinUia.Input.UnitTests/NativeLayoutTests.cs b/tests/WinUia.Core.UnitTests/Interop/NativeLayoutTests.cs
similarity index 89%
rename from tests/WinUia.Input.UnitTests/NativeLayoutTests.cs
rename to tests/WinUia.Core.UnitTests/Interop/NativeLayoutTests.cs
index b5ab45e..22e099a 100644
--- a/tests/WinUia.Input.UnitTests/NativeLayoutTests.cs
+++ b/tests/WinUia.Core.UnitTests/Interop/NativeLayoutTests.cs
@@ -1,7 +1,7 @@
using System.Runtime.InteropServices;
-using WinUia.Input.Interop;
+using WinUia.Core.Interop;
-namespace WinUia.Input.UnitTests;
+namespace WinUia.Core.UnitTests.Interop;
public class NativeLayoutTests
{
diff --git a/tests/WinUia.Core.UnitTests/WinUia.Core.UnitTests.csproj b/tests/WinUia.Core.UnitTests/WinUia.Core.UnitTests.csproj
index 345cef8..eb707c5 100644
--- a/tests/WinUia.Core.UnitTests/WinUia.Core.UnitTests.csproj
+++ b/tests/WinUia.Core.UnitTests/WinUia.Core.UnitTests.csproj
@@ -1,7 +1,6 @@
-
diff --git a/tests/WinUia.Input.IntegrationTests/WinUia.Input.IntegrationTests.csproj b/tests/WinUia.Input.IntegrationTests/WinUia.Input.IntegrationTests.csproj
deleted file mode 100644
index c336709..0000000
--- a/tests/WinUia.Input.IntegrationTests/WinUia.Input.IntegrationTests.csproj
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/tests/WinUia.Input.UiTests/WinUia.Input.UiTests.csproj b/tests/WinUia.Input.UiTests/WinUia.Input.UiTests.csproj
deleted file mode 100644
index c336709..0000000
--- a/tests/WinUia.Input.UiTests/WinUia.Input.UiTests.csproj
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/tests/WinUia.Input.UnitTests/WinUia.Input.UnitTests.csproj b/tests/WinUia.Input.UnitTests/WinUia.Input.UnitTests.csproj
deleted file mode 100644
index c336709..0000000
--- a/tests/WinUia.Input.UnitTests/WinUia.Input.UnitTests.csproj
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/tests/WinUia.IntegrationTests/AppLauncherTests.cs b/tests/WinUia.IntegrationTests/Launchers/AppLauncherTests.cs
similarity index 79%
rename from tests/WinUia.IntegrationTests/AppLauncherTests.cs
rename to tests/WinUia.IntegrationTests/Launchers/AppLauncherTests.cs
index 6c2abc3..18e04a8 100644
--- a/tests/WinUia.IntegrationTests/AppLauncherTests.cs
+++ b/tests/WinUia.IntegrationTests/Launchers/AppLauncherTests.cs
@@ -1,9 +1,7 @@
-using WinUia.Core;
-using WinUia.Launchers;
+using WinUia.Models;
-namespace WinUia.IntegrationTests;
+namespace WinUia.IntegrationTests.Launchers;
-/// Launcher failures: real processes and the real activation manager, but no window to drive.
public class AppLauncherTests
{
[Test]
@@ -13,7 +11,7 @@ public void GetMainWindow_fails_fast_when_the_process_exits()
{
using var context = new AutomationContext();
using var process = AppLauncher.LaunchExe("cmd.exe", new AppLaunchOptions { Arguments = "/c exit 3" });
- AppLauncher.GetMainWindow(context, process, TimeSpan.FromSeconds(15));
+ MainWindowLocator.Find(context, process, TimeSpan.FromSeconds(15));
});
Assert.That(ex.Message, Does.Contain("exited"));
}
@@ -43,12 +41,6 @@ public void LaunchExe_passes_the_working_directory_and_environment()
}
}
- [Test]
- public void Attach_to_a_missing_process_name_throws()
- {
- Assert.Throws(() => AppLauncher.Attach("WinUia.No.Such.Process"));
- }
-
[Test]
public void LaunchPackaged_with_an_unknown_AUMID_throws_an_AppProcessException()
{
diff --git a/tests/WinUia.IntegrationTests/AppTests.cs b/tests/WinUia.IntegrationTests/Model/AppTests.cs
similarity index 51%
rename from tests/WinUia.IntegrationTests/AppTests.cs
rename to tests/WinUia.IntegrationTests/Model/AppTests.cs
index 9f93d5f..a22a7da 100644
--- a/tests/WinUia.IntegrationTests/AppTests.cs
+++ b/tests/WinUia.IntegrationTests/Model/AppTests.cs
@@ -1,10 +1,10 @@
-namespace WinUia.IntegrationTests;
+namespace WinUia.IntegrationTests.Model;
public class AppTests
{
[Test]
public void Attaching_to_a_missing_process_throws_an_AppProcessException()
{
- Assert.Throws(() => App.Attach("WinUia.No.Such.Process"));
+ Assert.Throws(() => App.Attach(int.MaxValue));
}
}
diff --git a/tests/WinUia.IntegrationTests/WinUia.IntegrationTests.csproj b/tests/WinUia.IntegrationTests/WinUia.IntegrationTests.csproj
index 3ff6d18..4ace63f 100644
--- a/tests/WinUia.IntegrationTests/WinUia.IntegrationTests.csproj
+++ b/tests/WinUia.IntegrationTests/WinUia.IntegrationTests.csproj
@@ -1,7 +1,6 @@
-
diff --git a/tests/WinUia.NUnit.IntegrationTests/DesktopLockTests.cs b/tests/WinUia.NUnit.IntegrationTests/DesktopLockTests.cs
deleted file mode 100644
index b0e5dac..0000000
--- a/tests/WinUia.NUnit.IntegrationTests/DesktopLockTests.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-namespace WinUia.NUnit.IntegrationTests;
-
-public class DesktopLockTests
-{
- [Test]
- public void Disposing_the_lock_releases_the_desktop()
- {
- var first = DesktopLock.Acquire(TimeSpan.FromMinutes(5));
- first.Dispose();
- first.Dispose(); // Idempotent: must not release twice.
-
- using var second = DesktopLock.Acquire(TimeSpan.FromMinutes(5));
- Assert.That(second, Is.Not.Null);
- }
-
- [Test]
- public void Acquire_times_out_while_another_holder_keeps_the_desktop()
- {
- using var holder = DesktopLock.Acquire(TimeSpan.FromMinutes(5));
-
- Exception? caught = null;
- var other = new Thread(() =>
- {
- try { using var _ = DesktopLock.Acquire(TimeSpan.FromMilliseconds(200)); }
- catch (Exception ex) { caught = ex; }
- });
- other.Start();
- other.Join();
-
- Assert.That(caught, Is.TypeOf());
- }
-}
diff --git a/tests/WinUia.NUnit.IntegrationTests/Services/DesktopLockTests.cs b/tests/WinUia.NUnit.IntegrationTests/Services/DesktopLockTests.cs
new file mode 100644
index 0000000..59aca9a
--- /dev/null
+++ b/tests/WinUia.NUnit.IntegrationTests/Services/DesktopLockTests.cs
@@ -0,0 +1,63 @@
+
+namespace WinUia.NUnit.IntegrationTests.Services;
+
+public class DesktopLockTests
+{
+ [Test]
+ public void Disposing_the_lock_releases_the_desktop()
+ {
+ var first = DesktopLock.Acquire(TimeSpan.FromMinutes(5));
+ first.Dispose();
+ first.Dispose();
+
+ using var second = DesktopLock.Acquire(TimeSpan.FromMinutes(5));
+ Assert.That(second, Is.Not.Null);
+ }
+
+ [Test]
+ public void Acquire_times_out_while_another_holder_keeps_the_desktop()
+ {
+ using var holder = DesktopLock.Acquire(TimeSpan.FromMinutes(5));
+
+ Exception? caught = null;
+ var other = new Thread(() =>
+ {
+ try { using var _ = DesktopLock.Acquire(TimeSpan.FromMilliseconds(200)); }
+ catch (Exception ex) { caught = ex; }
+ });
+ other.Start();
+ other.Join();
+
+ Assert.That(caught, Is.TypeOf());
+ }
+
+ [Test]
+ public void Acquire_takes_over_the_desktop_from_a_holder_that_died_without_releasing_it()
+ {
+ // A thread that ends while holding the mutex abandons it, as a killed test host does.
+ var died = new Thread(() =>
+ {
+ var mutex = new Mutex(false, DesktopLock.Name);
+ mutex.WaitOne();
+ });
+ died.Start();
+ died.Join();
+
+ using var desktop = DesktopLock.Acquire(TimeSpan.FromSeconds(5));
+
+ Assert.That(desktop, Is.Not.Null);
+ }
+
+ [Test]
+ public void The_lock_can_be_released_from_another_thread_than_the_one_that_took_it()
+ {
+ var desktop = DesktopLock.Acquire(TimeSpan.FromMinutes(5));
+ var releaser = new Thread(desktop.Dispose);
+ releaser.Start();
+ releaser.Join();
+
+ using var next = DesktopLock.Acquire(TimeSpan.FromSeconds(5));
+
+ Assert.That(next, Is.Not.Null);
+ }
+}
diff --git a/tests/WinUia.NUnit.IntegrationTests/WinUia.NUnit.IntegrationTests.csproj b/tests/WinUia.NUnit.IntegrationTests/WinUia.NUnit.IntegrationTests.csproj
index ce162d8..61718a1 100644
--- a/tests/WinUia.NUnit.IntegrationTests/WinUia.NUnit.IntegrationTests.csproj
+++ b/tests/WinUia.NUnit.IntegrationTests/WinUia.NUnit.IntegrationTests.csproj
@@ -1,5 +1,3 @@
-
-
diff --git a/tests/WinUia.NUnit.UiTests/UiTestAttributeTests.cs b/tests/WinUia.NUnit.UiTests/Attributes/UiTestAttributeTests.cs
similarity index 93%
rename from tests/WinUia.NUnit.UiTests/UiTestAttributeTests.cs
rename to tests/WinUia.NUnit.UiTests/Attributes/UiTestAttributeTests.cs
index 3f30fea..80bc6f6 100644
--- a/tests/WinUia.NUnit.UiTests/UiTestAttributeTests.cs
+++ b/tests/WinUia.NUnit.UiTests/Attributes/UiTestAttributeTests.cs
@@ -1,11 +1,11 @@
-namespace WinUia.NUnit.UiTests;
+
+namespace WinUia.NUnit.UiTests.Attributes;
[UiTest]
public class UiTestAttributeTests
{
private bool _desktopHeldDuringSetUp;
- /// True when another thread cannot take the desktop right now.
private static bool DesktopIsHeld()
{
var held = false;
diff --git a/tests/WinUia.NUnit.UiTests/WinUia.NUnit.UiTests.csproj b/tests/WinUia.NUnit.UiTests/WinUia.NUnit.UiTests.csproj
index ce162d8..61718a1 100644
--- a/tests/WinUia.NUnit.UiTests/WinUia.NUnit.UiTests.csproj
+++ b/tests/WinUia.NUnit.UiTests/WinUia.NUnit.UiTests.csproj
@@ -1,5 +1,3 @@
-
-
diff --git a/tests/WinUia.NUnit.UnitTests/UiAssertionsTests.cs b/tests/WinUia.NUnit.UnitTests/Assertions/UiAssertionsTests.cs
similarity index 93%
rename from tests/WinUia.NUnit.UnitTests/UiAssertionsTests.cs
rename to tests/WinUia.NUnit.UnitTests/Assertions/UiAssertionsTests.cs
index 0ca9d7f..82c0ab4 100644
--- a/tests/WinUia.NUnit.UnitTests/UiAssertionsTests.cs
+++ b/tests/WinUia.NUnit.UnitTests/Assertions/UiAssertionsTests.cs
@@ -1,6 +1,6 @@
using NUnit.Framework.Internal;
-namespace WinUia.NUnit.UnitTests;
+namespace WinUia.NUnit.UnitTests.Assertions;
public class UiAssertionsTests
{
diff --git a/tests/WinUia.NUnit.UnitTests/WinUia.NUnit.UnitTests.csproj b/tests/WinUia.NUnit.UnitTests/WinUia.NUnit.UnitTests.csproj
index ce162d8..61718a1 100644
--- a/tests/WinUia.NUnit.UnitTests/WinUia.NUnit.UnitTests.csproj
+++ b/tests/WinUia.NUnit.UnitTests/WinUia.NUnit.UnitTests.csproj
@@ -1,5 +1,3 @@
-
-
diff --git a/tests/WinUia.TestApp/EmptyForm.cs b/tests/WinUia.TestApp/EmptyForm.cs
index b175534..daf0e96 100644
--- a/tests/WinUia.TestApp/EmptyForm.cs
+++ b/tests/WinUia.TestApp/EmptyForm.cs
@@ -1,6 +1,5 @@
namespace WinUia.TestApp;
-/// The test app's only window: a title and nothing else. Tests of control behaviour use the WinForms example.
internal sealed class EmptyForm : Form
{
public EmptyForm()
diff --git a/tests/WinUia.TestApp/Program.cs b/tests/WinUia.TestApp/Program.cs
index cd16a27..80d9720 100644
--- a/tests/WinUia.TestApp/Program.cs
+++ b/tests/WinUia.TestApp/Program.cs
@@ -2,7 +2,6 @@ namespace WinUia.TestApp;
internal static class Program
{
- /// Shows . With --ignore-close the window refuses to close, so it has to be killed.
[STAThread]
private static void Main(string[] args)
{
diff --git a/tests/WinUia.TestApp/WinUia.TestApp.csproj b/tests/WinUia.TestApp/WinUia.TestApp.csproj
index 345a396..85ed4d1 100644
--- a/tests/WinUia.TestApp/WinUia.TestApp.csproj
+++ b/tests/WinUia.TestApp/WinUia.TestApp.csproj
@@ -1,7 +1,5 @@
-
-
WinExe
true
diff --git a/tests/WinUia.Testing.Shared/AppPaths.cs b/tests/WinUia.Testing.Shared/AppPaths.cs
index dbe3d34..0b2d277 100644
--- a/tests/WinUia.Testing.Shared/AppPaths.cs
+++ b/tests/WinUia.Testing.Shared/AppPaths.cs
@@ -2,14 +2,8 @@
namespace WinUia.Testing.Shared;
-///
-/// Locations of the apps under tests/ that the library UI tests launch, stamped into this assembly by
-/// WinUia.Testing.Shared.csproj (one AppPath.<Name> metadata attribute per app). Add a property here for
-/// each new app.
-///
public static class AppPaths
{
- /// The test app, tests/WinUia.TestApp: an empty window.
public static string TestApp { get; } = Get("TestApp");
private static string Get(string name) =>
diff --git a/tests/WinUia.Testing.Shared/WinUia.Testing.Shared.csproj b/tests/WinUia.Testing.Shared/WinUia.Testing.Shared.csproj
index cbd01da..026fecb 100644
--- a/tests/WinUia.Testing.Shared/WinUia.Testing.Shared.csproj
+++ b/tests/WinUia.Testing.Shared/WinUia.Testing.Shared.csproj
@@ -1,12 +1,5 @@
-
-
-
diff --git a/tests/WinUia.UiTests/DerivedApp.cs b/tests/WinUia.UiTests/DerivedApp.cs
deleted file mode 100644
index 6572431..0000000
--- a/tests/WinUia.UiTests/DerivedApp.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace WinUia.UiTests;
-
-/// A page object that derives from , the way a user's main-window page object would.
-public sealed class DerivedApp : App
-{
- public string Title => MainWindow.Name;
-}
diff --git a/tests/WinUia.UiTests/AppLauncherTests.cs b/tests/WinUia.UiTests/Launchers/AppLauncherTests.cs
similarity index 69%
rename from tests/WinUia.UiTests/AppLauncherTests.cs
rename to tests/WinUia.UiTests/Launchers/AppLauncherTests.cs
index 5490176..00a3ee1 100644
--- a/tests/WinUia.UiTests/AppLauncherTests.cs
+++ b/tests/WinUia.UiTests/Launchers/AppLauncherTests.cs
@@ -1,9 +1,8 @@
using System.Diagnostics;
-using WinUia.Core;
-using WinUia.Launchers;
+using WinUia.Models;
using WinUia.Testing.Shared;
-namespace WinUia.UiTests;
+namespace WinUia.UiTests.Launchers;
public sealed class AppLauncherTests
{
@@ -41,7 +40,7 @@ public void LaunchExe_and_GetMainWindow_find_the_fixture_window()
{
var process = Track(AppLauncher.LaunchExe(AppPaths.TestApp));
- var window = AppLauncher.GetMainWindow(_context, process, TimeSpan.FromSeconds(15));
+ var window = MainWindowLocator.Find(_context, process, TimeSpan.FromSeconds(15));
using (Assert.EnterMultipleScope())
{
@@ -55,9 +54,9 @@ public void LaunchExe_and_GetMainWindow_find_the_fixture_window()
public void Close_closes_the_window_gracefully()
{
var process = Track(AppLauncher.LaunchExe(AppPaths.TestApp));
- AppLauncher.GetMainWindow(_context, process, TimeSpan.FromSeconds(15));
+ MainWindowLocator.Find(_context, process, TimeSpan.FromSeconds(15));
- AppLauncher.Close(_context, process, TimeSpan.FromSeconds(10));
+ AppCloser.Close(_context, process, TimeSpan.FromSeconds(10));
using (Assert.EnterMultipleScope())
{
@@ -70,9 +69,9 @@ public void Close_closes_the_window_gracefully()
public void Close_kills_a_window_that_refuses_to_close()
{
var process = Track(AppLauncher.LaunchExe(AppPaths.TestApp, new AppLaunchOptions { Arguments = "--ignore-close" }));
- AppLauncher.GetMainWindow(_context, process, TimeSpan.FromSeconds(15));
+ MainWindowLocator.Find(_context, process, TimeSpan.FromSeconds(15));
- AppLauncher.Close(_context, process, TimeSpan.FromSeconds(1));
+ AppCloser.Close(_context, process, TimeSpan.FromSeconds(1));
using (Assert.EnterMultipleScope())
{
@@ -82,18 +81,13 @@ public void Close_kills_a_window_that_refuses_to_close()
}
[Test]
- public void Attach_finds_a_running_process_by_id_and_name()
+ public void Attach_finds_a_running_process_by_id()
{
var process = Track(AppLauncher.LaunchExe(AppPaths.TestApp));
- AppLauncher.GetMainWindow(_context, process, TimeSpan.FromSeconds(15));
+ MainWindowLocator.Find(_context, process, TimeSpan.FromSeconds(15));
using var byId = AppLauncher.Attach(process.Id);
- using var byName = AppLauncher.Attach("WinUia.TestApp");
- using (Assert.EnterMultipleScope())
- {
- Assert.That(byId.Id, Is.EqualTo(process.Id));
- Assert.That(byName.ProcessName, Is.EqualTo("WinUia.TestApp"));
- }
+ Assert.That(byId.Id, Is.EqualTo(process.Id));
}
}
diff --git a/tests/WinUia.UiTests/AppTests.cs b/tests/WinUia.UiTests/Model/AppTests.cs
similarity index 91%
rename from tests/WinUia.UiTests/AppTests.cs
rename to tests/WinUia.UiTests/Model/AppTests.cs
index 5526652..a1d312a 100644
--- a/tests/WinUia.UiTests/AppTests.cs
+++ b/tests/WinUia.UiTests/Model/AppTests.cs
@@ -1,13 +1,9 @@
using System.Diagnostics;
-using WinUia.Core.Exceptions;
+using WinUia.Models;
using WinUia.Testing.Shared;
-namespace WinUia.UiTests;
+namespace WinUia.UiTests.Model;
-///
-/// Behaviour of itself: main window, Find timeouts, closing and ownership, against the empty test app.
-/// Finding controls and windows by content is tested in the WinForms example.
-///
public sealed class AppTests
{
private App _app = null!;
@@ -124,7 +120,7 @@ public void Launch_without_a_path_uses_the_page_objects_executable_and_defaults(
[Test]
public void Options_passed_to_Launch_override_the_page_objects_defaults()
{
- using var app = App.Launch(new AppLaunchOptions { ShowPointer = false });
+ using var app = App.Launch(options: new AppLaunchOptions { ShowPointer = false });
using (Assert.EnterMultipleScope())
{
diff --git a/tests/WinUia.UiTests/Model/DerivedApp.cs b/tests/WinUia.UiTests/Model/DerivedApp.cs
new file mode 100644
index 0000000..b67b62e
--- /dev/null
+++ b/tests/WinUia.UiTests/Model/DerivedApp.cs
@@ -0,0 +1,6 @@
+namespace WinUia.UiTests.Model;
+
+public sealed class DerivedApp : App
+{
+ public string Title => MainWindow.Name;
+}
diff --git a/tests/WinUia.UiTests/LaunchableApp.cs b/tests/WinUia.UiTests/Model/LaunchableApp.cs
similarity index 65%
rename from tests/WinUia.UiTests/LaunchableApp.cs
rename to tests/WinUia.UiTests/Model/LaunchableApp.cs
index 0efda48..ad90746 100644
--- a/tests/WinUia.UiTests/LaunchableApp.cs
+++ b/tests/WinUia.UiTests/Model/LaunchableApp.cs
@@ -1,8 +1,8 @@
+using WinUia.Models;
using WinUia.Testing.Shared;
-namespace WinUia.UiTests;
+namespace WinUia.UiTests.Model;
-/// A page object that knows its own executable and defaults, so it launches with App.Launch<LaunchableApp>().
public sealed class LaunchableApp : App
{
protected override string ExecutablePath => AppPaths.TestApp;
diff --git a/tests/WinUia.UiTests/WinUia.UiTests.csproj b/tests/WinUia.UiTests/WinUia.UiTests.csproj
index 248afd4..4616097 100644
--- a/tests/WinUia.UiTests/WinUia.UiTests.csproj
+++ b/tests/WinUia.UiTests/WinUia.UiTests.csproj
@@ -1,9 +1,7 @@
-
-
diff --git a/tests/WinUia.UnitTests/AppLaunchOptionsTests.cs b/tests/WinUia.UnitTests/Model/AppLaunchOptionsTests.cs
similarity index 93%
rename from tests/WinUia.UnitTests/AppLaunchOptionsTests.cs
rename to tests/WinUia.UnitTests/Model/AppLaunchOptionsTests.cs
index 98050d4..199221e 100644
--- a/tests/WinUia.UnitTests/AppLaunchOptionsTests.cs
+++ b/tests/WinUia.UnitTests/Model/AppLaunchOptionsTests.cs
@@ -1,4 +1,6 @@
-namespace WinUia.UnitTests;
+using WinUia.Models;
+
+namespace WinUia.UnitTests.Model;
public class AppLaunchOptionsTests
{
diff --git a/tests/WinUia.UnitTests/AppTests.cs b/tests/WinUia.UnitTests/Model/AppTests.cs
similarity index 89%
rename from tests/WinUia.UnitTests/AppTests.cs
rename to tests/WinUia.UnitTests/Model/AppTests.cs
index 2979f34..163df79 100644
--- a/tests/WinUia.UnitTests/AppTests.cs
+++ b/tests/WinUia.UnitTests/Model/AppTests.cs
@@ -1,4 +1,6 @@
-namespace WinUia.UnitTests;
+using WinUia.Models;
+
+namespace WinUia.UnitTests.Model;
public class AppTests
{
@@ -18,7 +20,7 @@ public void LaunchPackaged_rejects_options_Windows_decides_for_packaged_apps()
Assert.Throws(() =>
App.LaunchPackaged("Any.App_0000000000000!App", new AppLaunchOptions { WorkingDirectory = @"C:\" }));
Assert.Throws(() =>
- App.LaunchPackaged("Any.App_0000000000000!App",
+ App.LaunchPackaged("Any.App_0000000000000!App",
new AppLaunchOptions { Environment = new Dictionary { ["A"] = "1" } }));
}
}
diff --git a/tests/WinUia.UnitTests/Model/UnconnectedApp.cs b/tests/WinUia.UnitTests/Model/UnconnectedApp.cs
new file mode 100644
index 0000000..44cfa7c
--- /dev/null
+++ b/tests/WinUia.UnitTests/Model/UnconnectedApp.cs
@@ -0,0 +1,3 @@
+namespace WinUia.UnitTests.Model;
+
+public sealed class UnconnectedApp : App;
diff --git a/tests/WinUia.UnitTests/UnconnectedApp.cs b/tests/WinUia.UnitTests/UnconnectedApp.cs
deleted file mode 100644
index 71f192a..0000000
--- a/tests/WinUia.UnitTests/UnconnectedApp.cs
+++ /dev/null
@@ -1,4 +0,0 @@
-namespace WinUia.UnitTests;
-
-/// A derived app created directly instead of through As<TApp>(), so it is connected to nothing.
-public sealed class UnconnectedApp : App;
diff --git a/tests/WinUia.UnitTests/WinUia.UnitTests.csproj b/tests/WinUia.UnitTests/WinUia.UnitTests.csproj
index 3ff6d18..4ace63f 100644
--- a/tests/WinUia.UnitTests/WinUia.UnitTests.csproj
+++ b/tests/WinUia.UnitTests/WinUia.UnitTests.csproj
@@ -1,7 +1,6 @@
-