Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
root = true

# Win32 and COM interop declarations keep the Windows SDK names (INPUT, BM_CLICK, UIA_E_ELEMENTNOTAVAILABLE, ...),
# so each one can be looked up in the SDK headers and documentation as it is written there.
[**/Interop/*.cs]
resharper_inconsistent_naming_highlighting = none
127 changes: 0 additions & 127 deletions .github/CODE_OF_CONDUCT.md

This file was deleted.

11 changes: 0 additions & 11 deletions .github/FUNDING.yml

This file was deleted.

38 changes: 0 additions & 38 deletions .github/ISSUE_TEMPLATE/bug_report.md

This file was deleted.

20 changes: 0 additions & 20 deletions .github/ISSUE_TEMPLATE/feature_request.md

This file was deleted.

6 changes: 0 additions & 6 deletions .github/PULL_REQUEST_TEMPLATE.md

This file was deleted.

63 changes: 40 additions & 23 deletions .github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
<p align="center">
A .NET library for automating Windows applications through Microsoft UI Automation.
<br />
<a href="https://github.com/JelleBuning/win-uia/issues">Report Bug</a>
<a href="https://github.com/BuningSoftware/win-uia/issues">Report Bug</a>
·
<a href="https://github.com/JelleBuning/win-uia/issues">Request Feature</a>
<a href="https://github.com/BuningSoftware/win-uia/issues">Request Feature</a>
</p>
</div>

Expand Down Expand Up @@ -72,7 +72,7 @@ Setting up WinUia on your local machine is straightforward. Make sure the [.NET
1. **Clone the repository:**

```bash
git clone https://github.com/JelleBuning/win-uia.git
git clone https://github.com/BuningSoftware/win-uia.git
cd win-uia
```

Expand Down Expand Up @@ -113,18 +113,27 @@ 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; `As<T>()` turns a launched or
attached app into it, FlaUI-style. Page objects for part of the UI are plain classes built on an `Element`
(see `examples/WinUia.Examples.Winforms.Tests`):
A page object for the whole application derives from `App` and lists its controls; the generic factories
(`App.Launch<T>`, `App.LaunchPackaged<T>`, `App.Attach<T>`) 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`):

```csharp
public sealed class MainPage : 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?")); }
}

using var page = App.Launch(@"C:\path\to\MyApp.exe").As<MainPage>(); // disposing the page closes the app
using var page = App.Launch<MainPage>(); // its executable and defaults
using var quiet = App.Launch<MainPage>(new AppLaunchOptions { ShowPointer = false }); // one default overridden
using var other = App.Launch<MainPage>(@"D:\builds\MyApp.exe"); // another executable
// Disposing a page closes its app.
```

### Project structure
Expand All @@ -135,22 +144,30 @@ WinUia is a modular monolith: one solution, one project per module, and one test
|---|---|---|---|
| `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 | `Win32InputSimulator` (`SendInput`), `PhysicalDpi`, `NativeButton` | — |
| `WinUia.Input` | Platform | `IInputSimulator` and `Win32InputSimulator` (`SendInput`; each `AutomationContext` has one as `Input`, replaceable in tests), `PhysicalDpi` | — |
| `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 (`PhysicalDpi`, `NativeButton`), never its P/Invoke or COM declarations.
* 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.
* 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 `<Module>.UnitTests` project.
* Internals are shared only through `InternalsVisibleTo`, and only with the module's own test projects.
* Namespaces follow project and folder. One type per file.
* Package versions are set once, in `Directory.Packages.props`.

Tests live next to their module as `tests/<Module>.UnitTests`. The ones that drive a real application launch
Each module has up to three test projects under `tests/`, by what the tests need:

| Project | Tests | Needs |
|---|---|---|
| `<Module>.UnitTests` | The module's code on its own | Nothing |
| `<Module>.IntegrationTests` | Against the real UI Automation client, Win32 and other OS objects, without showing a window | Windows |
| `<Module>.UiTests` | Launch an app or move the real mouse and keyboard | An interactive, unlocked desktop |

Every test in a `.UiTests` project gets `[UiTest]` from the build (category `UI`), so
`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`. UI tests are marked `[UiTest]`
(category `UI`). Tests use NUnit.
`examples/WinUia.Examples.Winforms.Tests`. 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
Expand Down Expand Up @@ -189,13 +206,13 @@ Distributed under the GNU Affero General Public License v3.0 License. See `LICEN

<!-- MARKDOWN LINKS & IMAGES -->
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
[contributors-shield]: https://img.shields.io/github/contributors/JelleBuning/win-uia.svg?style=for-the-badge
[contributors-url]: https://github.com/JelleBuning/win-uia/graphs/contributors
[forks-shield]: https://img.shields.io/github/forks/JelleBuning/win-uia.svg?style=for-the-badge
[forks-url]: https://github.com/JelleBuning/win-uia/network/members
[stars-shield]: https://img.shields.io/github/stars/JelleBuning/win-uia.svg?style=for-the-badge
[stars-url]: https://github.com/JelleBuning/win-uia/stargazers
[issues-shield]: https://img.shields.io/github/issues/JelleBuning/win-uia.svg?style=for-the-badge
[issues-url]: https://github.com/JelleBuning/win-uia/issues
[license-shield]: https://img.shields.io/github/license/JelleBuning/win-uia.svg?style=for-the-badge
[license-url]: https://github.com/JelleBuning/win-uia/blob/main/LICENSE
[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
65 changes: 65 additions & 0 deletions .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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.
on:
release:
types: [published]

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

publish:
name: Publish to NuGet
needs: test
runs-on: windows-latest
environment: nuget
steps:
- name: Checkout
uses: actions/checkout@v7

- name: Setup .NET SDK
uses: actions/setup-dotnet@v6
with:
dotnet-version: '10.0.x'

- name: Version from the release tag
id: version
shell: bash
env:
TAG: ${{ github.event.release.tag_name }}
run: |
version="${TAG#v}"
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
-p:Version=${{ steps.version.outputs.version }} -p:ContinuousIntegrationBuild=true

- name: Keep the packages with the run
uses: actions/upload-artifact@v7
with:
name: packages
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 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
Loading