Skip to content

Migrate E2E harness from WinAppDriver to winapp ui#650

Merged
azchohfi merged 13 commits into
mainfrom
azchohfi-winappui-harness-migration
Jun 24, 2026
Merged

Migrate E2E harness from WinAppDriver to winapp ui#650
azchohfi merged 13 commits into
mainfrom
azchohfi-winappui-harness-migration

Conversation

@azchohfi

Copy link
Copy Markdown
Collaborator

Summary

Migrates the tests/Reactor.AppTests E2E harness from Appium/WinAppDriver to Microsoft's winapp CLI (winapp ui subcommands).

The full suite is green: 72 passed, 0 failed, 2 skipped.

Speed delta vs WinAppDriver baseline

  • Baseline (WAD): 331.9s, but with 6 failing input tests (their wait-timeouts inflate the wall-clock).
  • winapp ui: 177s, 0 failures.
  • Full-suite wall-clock is ~47% faster, but that is not pure apples-to-apples because of the 6 WAD failures.
  • Like-for-like headline: ~11% faster on the comparable UIA-operation subset.

Approach

Notable fixes for the input-injection tests

  • Per-Monitor-V2 DPI awareness at assembly load. winapp reports UIA bounds in physical pixels; a DPI-unaware process on a mixed-DPI multi-monitor desktop virtualizes metrics so SendInput normalization missed. No-op on all-100%-scale desktops (CI).
  • Robust Foreground(): AttachThreadInput to defeat the foreground lock, then verify the window actually became foreground (poll ~1s) and fail fast with a diagnosable exception. A bare SetForegroundWindow silently no-ops in batch runs when focus drifted.
  • DataGrid: display-only cells only fire Tapped on the active window and the inline editor has no AutomationId, so cells are tapped via foreground+real-click and the editor is located via inspect before typing through UIA focus.
  • DockingInput tab-merge: driven through the immediate tear-off pipeline with a pre-drag-computed drop target; each tab is selected before asserting since WinUI only surfaces the selected tab's content to UIA.

Scope

  • Zero net framework/Host change — only tests/Reactor.AppTests changed.
  • Clean build (0 warnings / 0 errors).

azchohfi and others added 3 commits June 22, 2026 15:40
…pp ui

Replace the Appium.WebDriver + WinAppDriver.exe harness with Microsoft's
winapp CLI (UIA-based, process-per-call). The app is driven via a WinAppUi
subprocess wrapper; a thin Win32 SendInput InputInjector handles the input
tests; a UiaPropertyReader (COM) fills winapp's property-read gaps. Base-class
helper signatures are unchanged so most test bodies are untouched.

- Add WinAppUi, UiElement, UiaPropertyReader, InputInjector, Keys, WinAppException.
- Rewrite TestSession/WinFormsTestSession to launch the Host and capture PID+HWND;
  delete WinAppDriverHelper.
- Re-point AppTestBase/WinFormsTestBase at WinAppUi/InputInjector.
- Port all test files off OpenQA/Appium; OnTapped now uses a real pointer tap.
- Remove Appium.WebDriver from the csproj and Directory.Packages.props.

Input-injection tests gracefully skip (Inconclusive) when the process cannot
reach the interactive input desktop (GetCursorPos/SendInput return ACCESS_DENIED
for non-uiAccess processes) instead of hard-failing, mirroring the existing
locked-desktop guard. They execute normally on a real interactive desktop.
Native winapp verbs (winappCli #562 send-keys, #498 drag) will later replace
the SendInput fallback.

Result: 48 passed / 0 failed / 26 skipped, 294.2s (vs WinAppDriver baseline
66/6/2, 331.9s) -- 11.4% faster wall-clock, no failures.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rd opt-out

Follow-up to the winapp migration, addressing review asks:

- Extract IUiaPropertyReader so the CUIAutomation COM fallback can be swapped
  for `winapp ui get-property` once winappCli exposes the missing properties.
  Document the empirically-verified null-property list (winapp 0.3.2 returns
  null for LocalizedControlType, IsRequiredForForm, FullDescription,
  HeadingLevel, LandmarkType, Level, PositionInSet, SizeOfSet, ItemStatus,
  LiveSetting; it DOES return Name/HelpText/AccessKey) with a swap-out TODO.
- Instrument WinAppUi.InvocationCount and record per-test winapp process-spawn
  counts (WinAppMetrics -> winapp-invocations.csv; also TestContext output) so
  the process-per-call overhead is measurable: ~439 spawns / 72 tests, avg 6.1.
- Add E2E_SKIP_LOCK_GUARD opt-out: winapp's UIA verbs work cross-desktop, so
  the UIA-only tests can run even when the console session is locked/disconnected
  (e.g. operator on RDP); input-injection tests still self-skip via the
  injectability guard. Off by default.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ab-merge

Resolve the remaining input-injection failures in the winapp-ui harness
migration. All AppTests now pass (72 passed, 2 skipped, 0 failed).

InputInjector:
- Mark the test process Per-Monitor-V2 DPI aware at assembly load. winapp
  reports UIA bounds in physical pixels; a DPI-unaware process on a mixed-DPI
  multi-monitor desktop virtualizes GetSystemMetrics/GetCursorPos, so the
  SendInput absolute-coordinate normalization mapped onto the wrong pixel and
  injected input missed. No-op on all-100%-scale desktops (e.g. CI).
- Make Foreground() robust: attach to the target thread's input queue to
  defeat the foreground lock, then SetForegroundWindow/BringWindowToTop/
  SetActiveWindow, verify the window actually became foreground (poll ~1s),
  and throw a diagnosable WinAppException if it never does. A bare
  SetForegroundWindow silently no-ops in batch runs when focus has drifted.
- Add CollapseSelectionToEnd + DragTearOffMerge helpers.

DataGridTests: display-only cells fire Tapped only on the active window and
the inline editor has no AutomationId, so tap cells via InputInjector
(foreground + real click) and locate the editor via winapp inspect before
typing through UiElement.SendKeys (foreground + UIA-focus + type).

DockingInputTests: drive the tab-merge through the immediate tear-off pipeline
(DragTearOffMerge) with a pre-drag-computed drop target, and select each tab
before asserting since WinUI only surfaces the selected tab's content to UIA.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
These E2E tests were never executed in CI before. Add an e2e-tests job to
ci.yml that installs the winapp CLI via microsoft/setup-WinAppCli, builds the
test project + Host (Debug/x64 — the path TestSession.FindHostExe expects),
and runs the suite. The job is gated on non-md changes like the other test
jobs. winapp ui's UIA reads + the harness's robust foreground/SendInput path
let these run on the interactive windows-latest hosted runner.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread tests/Reactor.AppTests/Infrastructure/UiElement.cs Fixed
Comment thread tests/Reactor.AppTests/Infrastructure/WinAppUi.cs Fixed
Comment thread tests/Reactor.AppTests/Infrastructure/WinAppUi.cs
Comment thread tests/Reactor.AppTests/Infrastructure/WinAppUi.cs
Comment thread tests/Reactor.AppTests/Infrastructure/WinAppUi.cs Fixed
Comment thread tests/Reactor.AppTests/Tests/DockingTearOffE2ETests.cs Fixed
Comment thread tests/Reactor.AppTests/Infrastructure/UiElement.cs Fixed
Comment thread tests/Reactor.AppTests/Infrastructure/WinAppUi.cs Fixed
azchohfi and others added 5 commits June 23, 2026 14:40
This E2E test landed on main after the harness migration branched, so it
still used the Appium API (OpenQA.Selenium, Session.FindElement/MobileBy/
WebDriverException). After merging main, port it onto the winapp harness:
drop the OpenQA usings and replace the hand-rolled absence poll with the
purpose-built App.WaitForGone helper.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The E2E suite (tests/Reactor.AppTests) now drives the app through the
winapp CLI instead of WinAppDriver. Add a best-effort winget install of
Microsoft.WinAppCli to bootstrap.ps1 (idempotent; skippable via
-SkipWinAppCli) so the suite runs out of the box, plus a run hint in the
closing summary. A missing winget only warns and never fails bootstrap.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The E2E harness migrated to the winapp CLI; refresh the contributor-facing
docs that still described the old toolchain: AGENTS.md, TESTING.md,
CONTRIBUTING.md, README.md, SKILL.md, the generated testing guide (edited via
its docs/_pipeline template + recompiled), and the pr-review test-coverage
tier doc. Documents how to install the winapp CLI (winget Microsoft.WinAppCli
or bootstrap.ps1). Historical specs/research records are left as-is.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Encapsulate the two static-field writes flagged by CodeQL behind static
  helpers (WinAppUi.InvocationCount via RecordInvocation; UiElement's
  _lastTextSendSelector via RememberTextSendAndWasRepeat), keeping the
  process-global semantics while moving the mutation off the instance path.
- Express the genuinely clearer filters with LINQ: ContainsTypedText -> Any,
  FindWindowHwnd -> Where, the Edit-finder inner loop -> Where, and the
  DockingTearOff diagnostics Read helper -> Select + FirstOrDefault.

The recursive ancestor-tab walk and the outer window loop keep their explicit
foreach: they guard on out-var TryGetProperty / accumulate via ref, where LINQ
would reduce clarity rather than improve it. No behavior change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread tests/Reactor.AppTests/Infrastructure/InputInjector.cs
Comment thread tests/Reactor.AppTests/Infrastructure/InputInjector.cs
Comment thread tests/Reactor.AppTests/Infrastructure/InputInjector.cs
Comment thread tests/Reactor.AppTests/Infrastructure/InputInjector.cs Fixed
Comment thread tests/Reactor.AppTests/Infrastructure/InputInjector.cs Fixed
Comment thread tests/Reactor.AppTests/Infrastructure/InputInjector.cs Fixed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR migrates the tests/Reactor.AppTests E2E harness from Appium/WinAppDriver to Microsoft’s winapp ui CLI, replacing the persistent driver session with per-call CLI invocations plus a small Win32 SendInput fallback for scenarios winapp ui can’t synthesize (typing/drag/press-hold). It also updates CI + contributor docs so the new harness is installable and runnable by default.

Changes:

  • Replace Appium session plumbing with a new WinAppUi process wrapper, UiElement shim, and UIA COM fallback reader for properties winapp ui can’t surface.
  • Add InputInjector (SendInput) for keystroke-level input + drag/gesture scenarios, and add per-test winapp invocation metrics logging.
  • Update repo docs + bootstrap + CI workflow to install and run winapp ui-driven E2E tests.

Reviewed changes

Copilot reviewed 42 out of 42 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
tests/Reactor.AppTests/Tests/WrapElementSlotTests.cs Switch absent-element wait to App.WaitForGone()
tests/Reactor.AppTests/Tests/WinFormsInteropTests.cs Update docs/intent to winapp ui-based driving
tests/Reactor.AppTests/Tests/TreeViewInteractionTests.cs Replace WinAppDriver element ops with App.Search + UIA reader
tests/Reactor.AppTests/Tests/NavigationViewInteractionTests.cs Replace WinAppDriver waits/clicks with winapp ui searches
tests/Reactor.AppTests/Tests/InteractiveTests.cs Update suite-level docs to winapp ui + SendInput
tests/Reactor.AppTests/Tests/ImmediateAndDisabledFocusableTests.cs Remove Appium/Selenium dependencies
tests/Reactor.AppTests/Tests/GestureTests.cs Use InputInjector for drag/hold; winapp click for others
tests/Reactor.AppTests/Tests/EventHandlerTests.cs Use real pointer clicks/SendInput where WinUI requires it
tests/Reactor.AppTests/Tests/DragDropTests.cs Replace Actions-based drag with InputInjector.Drag()
tests/Reactor.AppTests/Tests/DockingTearOffE2ETests.cs Replace Desktop-root Appium session with HWND enumeration + SendInput
tests/Reactor.AppTests/Tests/DockingInputTests.cs Replace tab-merge drag + typing with InputInjector + winapp helpers
tests/Reactor.AppTests/Tests/DevtoolsUxTests.cs Remove Appium dependency
tests/Reactor.AppTests/Tests/DataGridTests.cs Switch to cell-tap via SendInput + editor discovery via inspect
tests/Reactor.AppTests/Tests/ChartAccessibilityTests.cs Update harness docs to winapp ui
tests/Reactor.AppTests/Tests/AccessibilityTests.cs Swap exception type + remove implicit-wait plumbing
tests/Reactor.AppTests/Tests/AccessibilityInteractionTests.cs Update element lookup to new harness types
tests/Reactor.AppTests/Reactor.AppTests.csproj Remove Appium.WebDriver package reference
tests/Reactor.AppTests/Infrastructure/WinFormsTestSession.cs New WinForms host bootstrap via PID/HWND + WinAppUi
tests/Reactor.AppTests/Infrastructure/WinFormsTestBase.cs Replace driver helpers with WinAppUi/UiElement equivalents
tests/Reactor.AppTests/Infrastructure/WinAppUi.cs New winapp CLI wrapper (process + JSON parsing + waits/actions)
tests/Reactor.AppTests/Infrastructure/WinAppMetrics.cs Best-effort CSV metrics for per-test winapp invocations
tests/Reactor.AppTests/Infrastructure/WinAppException.cs New exception types replacing WebDriverException
tests/Reactor.AppTests/Infrastructure/WinAppDriverHelper.cs Remove WinAppDriver bootstrap helper
tests/Reactor.AppTests/Infrastructure/UiElement.cs New lightweight element handle (invoke/click/read/sendkeys/rect)
tests/Reactor.AppTests/Infrastructure/UiaPropertyReader.cs UIA COM fallback reader for properties missing in winapp
tests/Reactor.AppTests/Infrastructure/TestSession.cs Replace Appium session bootstrap with WinAppUi-based bootstrap
tests/Reactor.AppTests/Infrastructure/SessionInteractivityGuard.cs Split “interactive UIA” vs “input-injectable” gating
tests/Reactor.AppTests/Infrastructure/Keys.cs Replace Selenium Keys constants
tests/Reactor.AppTests/Infrastructure/IUiaPropertyReader.cs Interface to isolate UIA fallback implementation
tests/Reactor.AppTests/Infrastructure/InputInjector.cs New SendInput fallback (PMv2 DPI + foregrounding + drag/typing)
tests/Reactor.AppTests/Infrastructure/AppTestBase.cs Rewrite base test helpers around WinAppUi/UiElement
TESTING.md Update E2E suite docs from WinAppDriver to winapp ui
SKILL.md Update quick commands to reference winapp ui E2E
README.md Update repo layout blurb for E2E harness
docs/guide/testing.md Update user guide testing tier descriptions
docs/_pipeline/templates/testing.md.dt Update doc template (source of generated guide)
Directory.Packages.props Remove Appium.WebDriver pinned version
CONTRIBUTING.md Update contributor test command docs
bootstrap.ps1 Add best-effort WinAppCli install + opt-out flag
AGENTS.md Update agent docs for E2E driver
.github/workflows/ci.yml Add winapp CLI setup + new E2E test job
.github/skills/pr-review/dimensions/test-coverage.md Update skill docs to winapp ui E2E tier wording
Comments suppressed due to low confidence (1)

tests/Reactor.AppTests/Infrastructure/SessionInteractivityGuard.cs:123

  • Comment still refers to WebDriverException even though the harness now throws WinAppException. This can confuse readers when diagnosing failures.
        // Only reclassify when we have positive evidence the desktop is
        // unreachable. Active and Unknown both fall through and the original
        // WebDriverException is rethrown — masking a real failure as
        // Inconclusive on Unknown would lose signal in the diagnostic loop

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/Reactor.AppTests/Infrastructure/AppTestBase.cs Outdated
Comment thread tests/Reactor.AppTests/Infrastructure/WinFormsTestBase.cs Outdated
Comment thread tests/Reactor.AppTests/Infrastructure/UiaPropertyReader.cs
Comment thread tests/Reactor.AppTests/Tests/DataGridTests.cs
Comment thread tests/Reactor.AppTests/Infrastructure/WinAppUi.cs
Comment thread tests/Reactor.AppTests/Infrastructure/WinAppUi.cs
azchohfi and others added 2 commits June 24, 2026 11:49
Preserve nullable AutomationIds for slug selectors, surface winapp list-windows failures, avoid raw key sentinels, and keep UIA fallback string formatting from treating unsupported COM values as real text.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Address PR review findings in the winapp ui E2E harness: split pointer clicks from UIA invokes, harden winapp resolution/fallbacks and SendInput handling, enforce exact UIA lookups, add helper coverage, and make CI fail if input-injection E2Es are skipped environmentally.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread tests/Reactor.AppTests/Infrastructure/WinAppUi.cs Fixed
Comment thread tests/Reactor.AppTests/Infrastructure/SessionInteractivityGuard.cs Fixed
Comment thread tests/Reactor.AppTests/Infrastructure/SessionInteractivityGuard.cs Fixed
azchohfi and others added 2 commits June 24, 2026 13:36
…ion tests

The "Fail on skipped input-injection E2Es" gate ran a repo-root TRX lookup, but
`dotnet test <csproj>` writes TestResults under the project dir, so the gate
threw "AppTests TRX results were not produced" even when every test passed,
turning the E2E job red on green runs. Pin the TRX with --results-directory
TestResults so the gate finds it.

Add [Retry(3)] to the 31 synthetic-input E2E tests (keyboard/pointer/drag/
gesture). Win32 SendInput is occasionally dropped before the Host window
foregrounds on the unattended CI desktop; a real regression still fails every
attempt. Removable once winappCli #562 (send-keys)/#498 (drag) ship native verbs.

Also harden InputInjector.ClearViaKeyboard with a 15ms Ctrl-latch delay so a
fast field can't observe a bare 'A' before the Ctrl key-down dequeues (the
"ahello island" select-all race).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Resolve the three outstanding github-code-quality findings on PR #650:

- WinAppUi.ResolveWinAppExe: filter PATH entries with .Where(Path.IsPathFully
  Qualified) instead of an in-loop guard+continue, so the foreach target is
  explicitly filtered.
- SessionInteractivityGuard: convert all seven user32/wtsapi32 P/Invokes from
  raw DllImport extern to source-generated LibraryImport (partial), the modern
  AOT/trim-friendly form. GetUserObjectInformation and WTSQuerySessionInformation
  are pinned to their -W exports because LibraryImport uses ExactSpelling and
  does not auto-suffix. Add AllowUnsafeBlocks (required by the generator, matching
  the sibling Reactor.AppTests.Host project).

GetProcessWindowStation was flagged as unused but is live in Diagnose() (window-
station name read-back); it is kept and modernized rather than deleted.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread tests/Reactor.AppTests/Infrastructure/WinAppUi.cs Dismissed
Comment thread tests/Reactor.AppTests/Infrastructure/InputInjector.cs Dismissed
Comment thread tests/Reactor.AppTests/Infrastructure/InputInjector.cs Dismissed
@azchohfi azchohfi merged commit 610dd3c into main Jun 24, 2026
19 of 20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants