Skip to content

fix: resolve ArgumentException and lock screen trigger on WinUI 3 InputInjector#210

Draft
agneszitte wants to merge 16 commits intomainfrom
dev/agzi/fix-203-argumentexception-winui3
Draft

fix: resolve ArgumentException and lock screen trigger on WinUI 3 InputInjector#210
agneszitte wants to merge 16 commits intomainfrom
dev/agzi/fix-203-argumentexception-winui3

Conversation

@agneszitte
Copy link
Copy Markdown
Member

Summary

Fixes two bugs in the Windows/WinUI 3 (#else) input injection path that caused System.ArgumentException and unintended lock screen activation when using InputInjectorHelper on real Windows hardware.

Fixes #203

Root Causes

Bug 1: ReleaseAny() throws ArgumentException

ReleaseAny() in the #else (non-HAS_UNO) path combined InjectedInputMouseOptions.XUp with LeftUp | MiddleUp | RightUp. The Windows InputInjector API requires the MouseData field to specify which X button (X1 or X2) when XUp is set. Without it, the API rejects the call with ArgumentException: 'value does not fall within the expected range.'.

Fix: Removed XUp from the #else ReleaseAny() path. The HAS_UNO path is unchanged (it checks actual button states).

Bug 2: CleanupPointers() triggers Windows hot corners / lock screen

CleanupPointers() called Mouse.MoveTo(0, 0), which on the #else path generates large negative relative deltas that physically move the real OS cursor to the upper-left corner of the screen. This triggers Windows hot corners, which can activate the lock screen.

Note: Bug 2 was masked by Bug 1 in practice — the ArgumentException would fire first during ReleaseAny(), preventing execution from reaching the MoveTo call.

Fix: Added #if HAS_UNO conditional to CleanupPointers(). On Uno/Skia, behavior is unchanged (MoveTo(0,0)). On WinUI 3, a new ResetTrackedPosition() method resets the internal _trackedPosition field without injecting any real mouse movement.

Changes

File Change
InputInjectorHelper.MouseHelper.cs Removed XUp from ReleaseAny() #else path; added ResetTrackedPosition() method
InputInjectorHelper.cs Added #if HAS_UNO conditional in CleanupPointers()
PointersInjectionTests.cs Added When_ReleaseAny_DoesNotThrow and When_CleanupPointers_DoesNotThrow regression tests

Validation

  • Build: Verified on both net10.0-windows10.0.22621 and net10.0-desktop targets
  • Skia Desktop tests: Both new tests pass; no regressions introduced (all failures are pre-existing: No_Longer_Sane intentional, HotReloadTests/SecondaryAppTests require DevServer, TapCoordinates failures are pre-existing on Skia Desktop)
  • Note: Full WinUI 3 runtime validation requires a Windows machine with inputInjectionBrokered capability — these are the exact code paths the reporting client exercises

…utInjector

- Remove XUp from ReleaseAny() on #else path: Windows InputInjector API
  requires MouseData to specify which X button; sending XUp without it
  throws ArgumentException
- Add ResetTrackedPosition() to MouseHelper for #else path: resets
  internal tracked position without injecting real mouse movement
- Update CleanupPointers() with #if HAS_UNO conditional: on Uno keeps
  MoveTo(0,0), on WinUI 3 uses ResetTrackedPosition() to avoid
  physically moving the OS cursor to top-left corner which triggers
  Windows hot corners and lock screen
- Add regression tests for ReleaseAny and CleanupPointers
@agneszitte agneszitte marked this pull request as draft March 25, 2026 19:48
…s pre-existing warnings

- Wrap EnableConfigPersistence and StoreConfig with try/catch to handle
  InvalidOperationException when ApplicationData.Current is unavailable
  (unpackaged WinUI 3 apps have no package identity)
- Suppress pre-existing CA1873 and CS8619 warnings in Directory.Build.props
- Copy Log: copies human-readable test log (status, counts, all test
  results with icons, error messages, and console output)
- Copy XML: copies NUnit XML results (previous behavior)
- Groups results by test class for readability
…EAD on WinUI 3

IsSecondaryApp is a DependencyProperty read from a background thread
in ExecuteTestsForInstance. On WinUI 3, this throws COMException
(0x8001010E). Cache the value in UnitTestEngineConfig from the UI
thread via BuildConfig() before dispatching to Task.Run.
MoveNoCoalesce (0x2000) alone is not a valid mouse input flag on Windows.
The OS InputInjector API requires MOUSEEVENTF_MOVE (0x1) to recognize
the input as a movement event. Without it, InjectMouseInput throws
ArgumentException on WinUI 3.
- Remove MoveNoCoalesce flag from MoveBy (not in any MSDN example, may be
  rejected by the WinRT InputInjector API on desktop apps)
- Remove TimeOffsetInMilliseconds from all mouse input constructors (official
  MSDN examples never set it, defaulting to 0)
- Materialize IEnumerable to array (.ToArray()) before passing to the Windows
  InputInjector.InjectMouseInput API (all official examples use new[] { ... },
  lazy LINQ chains may not marshal correctly via CsWinRT projection)
On real Windows, passing large arrays of InjectedInputMouseInfo
to InputInjector.InjectMouseInput through the WinRT interop layer
causes ArgumentException. Injecting events individually avoids
the issue while maintaining the same behavior on Uno (HAS_UNO path).
On WinUI 3, InputInjector.InjectMouseInput with Move flag uses relative
deltas from the physical OS cursor position. The tracked position (which
resets to 0,0) does not correspond to the actual cursor position, so
MoveTo computes wrong deltas and the cursor misses the target element.

Fix: In Tap(UIElement), use Win32 SetCursorPos + ClientToScreen to
position the cursor at the exact screen coordinates of the element
center, then inject Press/Release at that location. DIP-to-pixel
conversion uses XamlRoot.RasterizationScale for DPI awareness.

Also adds MouseHelper.SetTrackedPosition() to keep the tracked position
in sync after direct cursor positioning.
…n WinUI 3

- GetActiveWindow/GetForegroundWindow return null (0x0) for XAML-island child
  HWNDs when thread focus changes during test execution
- Use Process.GetCurrentProcess().MainWindowHandle to reliably find the
  top-level window for SetWindowPos repositioning
- Ensures EnsureClientAreaOnScreen actually moves the root window when
  test UI elements extend off-screen (Y=-280 clamped to Y=0 by SetCursorPos)
- Add cursor-verification guard in Tap() that throws descriptive
  InvalidOperationException when SetCursorPos cannot reach the target
- Remove unused GetParent P/Invoke declaration
@agneszitte agneszitte force-pushed the dev/agzi/fix-203-argumentexception-winui3 branch 2 times, most recently from e286b78 to b37bbeb Compare March 26, 2026 01:09
SecondaryApp.IsSupported returns true on Windows via
OperatingSystem.IsWindows(), but DevServer is intentionally excluded
on Windows/WinUI 3 (VS provides native Hot Reload instead).

The original #if __SKIA__ check was correct in spirit — SecondaryApp
cannot actually run tests on WinUI 3 due to missing DevServer — but
the assertion failed because IsSupported checks the OS, not DevServer
availability.

Changed the test to use #if HAS_UNO_DEVSERVER / __SKIA__ / else
branches that match the actual runtime capability instead of making
a hard true/false assertion that contradicts the property value.
… errors

- SecondaryApp.IsSupported now checks HAS_UNO_DEVSERVER in addition to
  OS check, so it returns false on WinUI 3 where DevServer is not available
- HotReloadTests now uses ignoreIfNotSupported: true on RunsInSecondaryApp
- Is_SecondaryApp_Supported test simplified to match corrected property
…esilience

- Use Process.MainWindowHandle instead of GetActiveWindow() for
  ClientToScreen to avoid XAML island child HWND mismatch
- Add EnsureClientAreaOnScreen fallback when target coordinates
  are outside the virtual screen area
- Add BringAppToForeground via SetForegroundWindow before each Tap
  to ensure injected input reaches the test app window
- Add GetForegroundWindow as last-resort HWND fallback
- Wrap PointersInjectionTests Setup/Cleanup in try-catch to prevent
  cascading COMException failures across subsequent tests
- Update No_Longer_Sane expected failure message
- Mouse: use Absolute|VirtualDesk normalized coordinates through InputInjector
  instead of SetCursorPos (which only moves OS cursor, not InputInjector's
  internal position)
- Touch: use Win32 Touch Injection API (user32.dll InitializeTouchInjection +
  InjectTouchInput) instead of WinRT InputInjector.InjectTouchInput (which
  targets UWP CoreWindow and silently fails on WinUI 3's Win32 windowing)
- Add comprehensive diagnostics for mouse and touch injection paths
- Add PointerPressed/PointerReleased tracking in pointer injection tests
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.

CurrentPosition() throws NRE in WinUI

1 participant