WpfTestKit.Xunit 0.1.1

dotnet add package WpfTestKit.Xunit --version 0.1.1
                    
NuGet\Install-Package WpfTestKit.Xunit -Version 0.1.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="WpfTestKit.Xunit" Version="0.1.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="WpfTestKit.Xunit" Version="0.1.1" />
                    
Directory.Packages.props
<PackageReference Include="WpfTestKit.Xunit" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add WpfTestKit.Xunit --version 0.1.1
                    
#r "nuget: WpfTestKit.Xunit, 0.1.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package WpfTestKit.Xunit@0.1.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=WpfTestKit.Xunit&version=0.1.1
                    
Install as a Cake Addin
#tool nuget:?package=WpfTestKit.Xunit&version=0.1.1
                    
Install as a Cake Tool

WpfTestKit

Reusable WPF testability infrastructure. Drops into any WPF app to give it a trace log, hidden UIA observability surface, DI-friendly service contracts, optional batch-job plumbing, and a FlaUI-based out-of-process click-test harness with auto screenshot + UIA-tree capture on failure.

The kit ships in two packages:

  • WpfTestKit — diagnostics, services, batch, FlaUI helpers. Reference from your WPF app.
  • WpfTestKit.XunitFlaUiTestFixture base class. Reference from your test project.

What your WPF app must do to use the kit

  1. Put <diagnostics:DiagnosticsUiaSurface /> somewhere in MainWindow. Without it, FlaUI can't read the trace log out of the running process.
  2. Put AutomationProperties.AutomationId on every element a test will touch.

That's the hard contract. Everything else is recommended but optional:

  • DI is recommended but not required. AddWpfTestKit* extensions are sugar; new DiagnosticsService() works fine.
  • CommunityToolkit.Mvvm is not a kit dependency. The kit uses its own hand-rolled INotifyPropertyChanged. Your VMs are free to use CTK, ReactiveUI, or nothing; the kit doesn't care and won't drag CTK into consumers.
  • Your async commands don't have to use IBackgroundJobService. It's a testability tool for VM unit tests; FlaUI tests work regardless.

Consumer-side startup wiring is under 50 lines — see the example at the end of this file.

Where to go next

  • Writing more than a handful of tests? Read the Testing patterns section below.
  • Looking up a specific capability? Skip to the feature matrix at the bottom.

Testing patterns

The kit's primitives (full reference in the feature matrix below) compose into patterns that keep tests fast, deterministic, and maintainable. Pick the ones that fit.

Structuring tests

Arrange / Act / Assert. Launch → drive → assert. Dispose is automatic via FlaUiTestFixture.

[Fact]
public void IncrementButton_ClickedThrice_CounterReadsThree()
{
    // Arrange
    var app = MyAppTestApp.Launch();
    SetCurrentApp(app);

    // Act
    for (int i = 0; i < 3; i++) { app.InvokeById("IncrementButton"); }

    // Assert
    Assert.Equal("3", app.WaitForText("CounterText", "3", timeoutSeconds: 3));
    app.AssertNoDiagnosticsErrors(Output.WriteLine);
}

TestApp wrapper per app. Don't repeat Launch(exePath, args) in every test. One helper per app keeps scenario tests concise and lets you push common startup args (test-mode flags, stubbed services) into one place.

internal static class MyAppTestApp
{
    public static TestApp Launch(string? arguments = null)
        => TestApp.Launch(TestApp.LocateExeRelativeToSolution("MyApp"), arguments);

    // Common test-mode variants — adopt as needs surface:
    public static TestApp LaunchWithStubbedFileDialog(string? pathToOpen)
        => Launch($"--test-file-to-open={pathToOpen ?? "CANCEL"}");
}

Screen-object helpers for repeated flows. When a five-line login/save/load sequence appears in eight tests, extract a static helper that takes a TestApp. Keep it thin — it's a named composition, not an abstraction layer.

internal static class LoginFlow
{
    public static void SignInAs(TestApp app, string user, string pw)
    {
        app.TypeInto("UsernameInput", user);
        app.TypeInto("PasswordInput", pw);
        app.InvokeById("SignInButton");
        app.WaitForText("WelcomeLabel", $"Welcome, {user}", timeoutSeconds: 5);
    }
}

Extending FlaUiTestFixture. If you want custom capture artifacts, startup checks, or per-suite diagnostics, override DisposeCore — don't subclass Dispose directly. The base already handles app disposal + artifact capture policy. See ArtifactCapturePolicy for Always / OnFailure / Never.

Element access

Prefer InvokeById / TypeInto over raw FindById. The one-shot helpers auto-retry until the element exists (default 3s). FindById returns AutomationElement? immediately — you lose auto-retry and have to handle null yourself. Use FindById only when you genuinely need a "is it there right now?" predicate.

Use Locator when touching the same element more than once. Locator re-queries UIA on every action, so it survives layout rebuilds (tab switches, collection changes, anything that invalidates the previous element reference).

var save = app.Locator("SaveButton");
save.Click();           // first interaction: fresh query
// ...UI may rebuild...
save.Click();           // re-queries; safe even after layout changes
var text = save.Read(); // same handle, different action

Use LocatorIn for modals and secondary windows. Same stale-proof behavior, but rooted at a given AutomationElement instead of MainWindow. Essential when InvokeAndWaitForModal returns a modal you want to drive with the same ergonomic API.

var modal = app.InvokeAndWaitForModal("DeleteButton", "Confirm", timeoutMs: 3000);
app.LocatorIn(modal!, "DialogOkButton").Click();

Waiting and synchronization

Never Thread.Sleep to wait for state. The kit ships four waiters; one of them fits every real case.

You want to wait for… Use
A label to contain a substring app.WaitForText(id, "done", timeoutSeconds: N)
An element to appear in the UIA tree app.WaitForElement(id, timeoutMs: N)
A button to become enabled app.WaitForButtonEnabled(id, timeoutMs: N)
Anything else (custom predicate) app.WaitFor(() => predicate) or app.WaitForOrThrow(() => predicate, "description")

Thread.Sleep is only acceptable for documented UIA-level races (e.g. the 150ms settle after SelectTabItem, already in the kit). Your test code shouldn't need it.

Don't add manual retry around kit calls. Auto-retry is already there. InvokeById("Foo") retries until DefaultActionTimeoutMs (3s). If you need a longer budget, pass timeoutMs: explicitly — don't wrap in a loop.

Use Retry(action, maxAttempts, delayMs) only for genuinely flaky scenarios that auto-retry can't fix (e.g. a background service that only settles after an external event). Document why — future-you will thank you.

Async commands and background work

Trigger the command, then WaitForText the observable result. Don't poll internal VM state from a FlaUI test — you can't see it anyway, and the label is the user-visible contract.

app.InvokeById("StartSlowJobButton");
var status = app.WaitForText("JobStatusLabel", "Completed", timeoutSeconds: 30);
Assert.Equal("Completed", status);

For the VM-level unit tests you should write separately from FlaUI tests, swap the production IBackgroundJobService for InlineBackgroundJobService (runs synchronously) or GatedBackgroundJobService (blocks until you Release()) so you can observe mid-flight state without races.

Modals

Expected modal → InvokeAndWaitForModal. Fires the trigger on a background task so the modal's nested message loop can't block the test thread. Returns the modal window; drive it with LocatorIn or InvokeButtonByName / InvokeButtonById.

Unexpected modals → StartModalWatchdog. Stands up a background poller that auto-dismisses pop-ups using a safe default button order (Cancel/No/Close/OK/Yes). Every test that doesn't expect a modal should have one.

using var watchdog = app.StartModalWatchdog();
// ...exercise flow that shouldn't open anything...
Assert.Equal(0, watchdog.DismissedCount);   // hard failure if it did

Combine both patterns: one test expects "Confirm deletion", every other test uses the watchdog as a safety net.

Assert on the modal's content, not just that it appeared.

app.AssertContainsText(modal!, "Proceed with delete?");
Assert.True(app.HasButton(modal!, "Yes"));
Assert.True(app.HasButton(modal!, "No"));

Keyboard

Pick the right layer for what you're actually testing:

Goal Use
Set a TextBox's value (bypass keyboard entirely) TypeInto(id, text) — ValuePattern, no focus needed
Exercise a KeyDown handler or IME SendKeys(text) — real synthesized keystrokes
Invoke a Ctrl+S-style shortcut PressShortcut("Ctrl+S")
Chord shortcut (Ctrl+K, Ctrl+E) or Alt-mnemonic (Alt+F, N) PressChord("Ctrl+K", "Ctrl+E")
Single key (Enter, Escape, F2) PressKey("Enter")

Reach for SendKeys / PressShortcut only when the keystroke handler is what's under test. For state setup, TypeInto is faster and doesn't require window focus.

Assertions

Assert against durable observable state. In order of preference:

  1. UIA propertyReadText, IsToggled, GetSelectedComboBoxText, CheckReachability. These are what the user experiences.
  2. DiagnosticsAssertNoDiagnosticsErrors(Output.WriteLine) at the end of every test. Catches silent exceptions, data-binding errors, and anything routed through IDiagnosticsService.
  3. Visual regressionAssertScreenshotMatches(baselineName, baselineDir, diffTolerancePct, perPixelTolerance) for layout-sensitive screens. First run establishes the baseline; subsequent runs diff. Adjust perPixelTolerance for ClearType anti-aliasing drift.
  4. ContrastAssertContrastRatio(id, minimumRatio: 4.5) for WCAG AA on text elements that should be readable.

Don't assert on things the user can't see (internal VM state, timing, exact pixel counts). If your test breaks on an innocuous refactor, the assertion was too narrow.

Reachability — guarding before you interact

For tests that care about layout (resizing, tab-content, scroll containers), assert reachability before the action:

app.ResizeMainWindow(400, 300);
app.AssertReachable("SaveButton");   // throws with reason if clipped, covered, off-screen
app.InvokeById("SaveButton");

Or for a whole-screen hygiene check:

var audit = app.AuditAutomationIds();
Assert.True(audit.IsClean,
    $"{audit.MissingOnInteractiveElements.Count} missing, {audit.DuplicateIds.Count} duplicate");

Reachability distinguishes NotFound / HitTestBlocked / ScrolledOutOfView / OutsideWindow / PartiallyClipped / ZeroSized / OffscreenViaUia with a reason string — failure messages tell you why it wasn't clickable, not just that it wasn't.

Diagnostics and triage

FlaUiTestFixture captures two artifacts on every failure by default — screenshot + UIA tree dump to %TEMP%\WpfTestKit\. Paths are echoed to test output.

Triage order when a test fails:

  1. Read the error message. IdSuggester already suffixes lookup failures with "Did you mean: 'XxxButton'?" — most typos resolve here.
  2. Open the screenshot. Is the app in the state you expected? Wrong tab, wrong modal, crashed?
  3. Open the UIA tree dump. Does the element you named actually exist? With that AutomationId? Under the expected parent?
  4. Re-run with TraceRecorder for step-level timing. Attach via app.Trace = new TraceRecorder(); dump HTML at end of test. Most useful for "it works sometimes" bugs.
  5. Mid-test app.Pause("why") to freeze the running app and inspect interactively. No-op when WPFTESTKIT_HEADLESS=1 — safe to leave committed.

Flakiness mitigation

The kit's auto-retry eliminates most manual flakiness control. The remaining levers:

  • Pick semantic waits over timeouts. WaitForText(id, "Done", seconds: 30) is far more robust than Thread.Sleep(30_000) — it returns as soon as state changes.
  • Isolate one app per test. The default FlaUiTestFixture gives you that. Sharing an app across tests to save launch time is almost never worth the cross-test contamination.
  • Use AssertNoDiagnosticsErrors in every test. Catches errors that would otherwise present as unrelated downstream flakes.
  • For known races, WaitFor(() => predicate) not Thread.Sleep. Returns on first success.
  • Screenshot diffing with tolerance. diffTolerancePct: 0.5, perPixelTolerance: 10 handles ClearType subpixel drift; tune higher if your test machine uses a different DPI than the baseline.
  • If a specific test is still flaky despite the above, wrap it with app.Retry(action, maxAttempts: 3) and leave a comment explaining the external cause. Don't retry as a blanket policy.

Recording new tests

Covered fully in User guide §6. Summary: when starting from scratch on an app, launch the recorder, drive the flow manually, copy generated [Fact] code, add assertions where the // TODO lands.

Anti-patterns

Things that look reasonable but cause flakes, false passes, or maintenance debt:

  • Thread.Sleep instead of a waiter. Auto-retry and WaitForText / WaitFor cover every real case. If you're tempted, re-read §Waiting.
  • Hardcoded paths to MyApp.exe. Use TestApp.LocateExeRelativeToSolution — tests run on any dev machine without path edits.
  • Asserting on timing (elapsed < 100ms). Test machines vary by 10×. Assert on behavior; measure timing separately if you care about performance.
  • Sharing a TestApp instance across tests. Cross-test state contamination is the #1 source of "passes alone, fails in suite" flakes. Default is one app per test — stick with it.
  • Skipping AssertNoDiagnosticsErrors. Binding errors, unhandled exceptions in command handlers, and async void throws all flow through it. Opting out means those bugs become next month's flake.
  • Using FindById + null check where InvokeById would do. You lose auto-retry and write more code. Only meaningful when you specifically want "is this visible right now?".
  • Asserting on AutomationElement.Name instead of AutomationId. Name is localized and changes with the user's OS language; AutomationId doesn't.
  • Pausing in CI. Set WPFTESTKIT_HEADLESS=1Pause() becomes a no-op. Never leave a Pause() call blocking CI.
  • Relying on focus for assertions. Focus state is fragile (foreground stealing, alt-tab). Use UIA property reads (IsToggled, ReadText) which work regardless of focus.
  • Adding a screenshot assertion to a fast-changing UI. Visual regression is for stable layouts (dialog chrome, splash screens). Scenery that shifts every sprint will just rot the baseline.
  • Testing implementation details (command names, VM property order). Test what the user sees through UIA. If you need VM-level coverage, write separate unit tests against the VM directly.

Feature × kit capability — extensive matrix

Legend: shipped and exercised by an end-to-end test · 🔧 shipped in kit, unit-tested only · 🚧 planned (design sketched) · 💭 idea (wanted; needs design)

Basic interaction

Feature Kit capability Status
Click a button by AutomationId (UIA Invoke pattern, no mouse movement) TestApp.InvokeById(id, timeoutMs?)auto-retries until the element exists (default 3s); works even when window is not focused, blocked, or over RDP ✅ CounterTests, AutoRetryTests
Real synthesized mouse click (cursor actually moves) TestApp.MouseClickById(id, timeoutMs?) — also auto-retries ✅ TextInputTests
Reusable chainable handle for an element TestApp.Locator(id).Click().Type(text).Read() — re-queries UIA on every call; stale-proof across layout rebuilds ✅ LocatorTests
Wait for a label to show a substring TestApp.WaitForText(id, needle, timeoutSeconds, ct) (CT-aware) ✅ CounterTests, SlowJobTests
Wait for an element to appear in the UIA tree TestApp.WaitForElement(id, timeoutMs, ct) (CT-aware) ✅ SecondaryTabTests
Wait for a button to become enabled TestApp.WaitForButtonEnabled(id, timeoutMs, ct) (CT-aware) 🔧
Wait for an arbitrary predicate TestApp.WaitFor(() => predicate) / WaitForOrThrow(() => predicate, "description") — uses WaitUtil.Until internally ✅ AutoRetryTests + WaitUtilTests
Get an element without waiting TestApp.FindById(id)AutomationElement? ✅ SecondaryTabTests
Set a TextBox's content (UIA ValuePattern, no keystrokes) TestApp.TypeInto(id, text) ✅ TextInputTests
Read a TextBox's current content TestApp.ReadText(id) — falls back to label text for non-Value elements ✅ TextInputTests
Synthesized keystroke typing for shortcuts / IME TestApp.SendKeys(text) ✅ KeyboardTests
Select a ComboBox item by visible text or index TestApp.SelectComboBoxItem(id, itemText) / SelectComboBoxIndex(id, i) — expands via ExpandCollapse, finds the item, invokes SelectionItem. Handles the virtualization trap. ✅ ComboBoxAndToggleTests
Read a ComboBox's currently selected item TestApp.GetSelectedComboBoxText(id) ✅ ComboBoxAndToggleTests
Toggle a CheckBox / RadioButton TestApp.SetToggle(id, bool) / IsToggled(id) — Toggle pattern or SelectionItem pattern; handles both WPF CheckBox and RadioButton ✅ ComboBoxAndToggleTests
Navigate nested menus (FileRecentmydoc.house) TestApp.InvokeMenuPath("File", "Recent", "mydoc.house") — walks the path, expanding each MenuItem before descending. Error includes path context on failure. ✅ NestedMenuTests
Assert a menu item exists without opening it TestApp.HasMenuItem(path...) — walks the structure, returns bool ✅ NestedMenuTests
Drag from one element's center to another's TestApp.DragFromTo(sourceId, targetId) — real synthesized mouse drag. Requires foreground. Tests assert cursor-final-position rather than WPF MouseEnter (capture semantics vary). ✅ DragAndDropTests
Drive a secondary window (modal or not) with full Locator API TestApp.LocatorIn(root, id) — creates a Locator scoped to any AutomationElement root (e.g., a modal returned by WaitForModalWindow) ✅ LocatorInTests
Retry a flaky action up to N times TestApp.Retry(action, maxAttempts, delayMs) / generic T Retry<T>(Func<T>, ...) — returns on first success, throws AggregateException with all errors if every attempt fails ✅ RetryUtilTests
Auto-record every kit action to a trace app.Trace = new TraceRecorder(); — every subsequent InvokeById / TypeInto / PressShortcut / etc. adds a step with timing. Combine with TraceRecorder.WriteHtmlReport for self-contained HTML on failure. ✅ TraceRecorderIntegrationTests
Measure WCAG contrast ratio of an element TestApp.EstimateContrastRatio(id) returns a 1.0–21.0 ratio; AssertContrastRatio(id, minimumRatio) throws below threshold. Heuristic: finds the pair of most-common colors inside the element with the highest luminance gap — handles anti-aliased text reliably. ✅ ContrastTests
Suggest similar AutomationIds when a lookup fails Every lookup-failure exception from InvokeById / DragFromTo etc. includes a " Did you mean: 'XxxButton', 'YyyLabel'?" suffix. Uses substring-match + Levenshtein distance. ✅ IdSuggestionTests

Tabs, dialogs, and secondary windows

Feature Kit capability Status
Select a TabItem (lazy-render trap) TestApp.SelectTabItem(tabId) (SelectionItem pattern + 150ms settle) ✅ SecondaryTabTests
Assert content invisible until tab selected FindById returns null before SelectTabItem ✅ SecondaryTabTests
Dismiss an expected modal dialog (MessageBox / Window.ShowDialog) TestApp.InvokeAndWaitForModal(triggerId, titleSubstring, timeoutMs) — fires the trigger on a background task (so the modal's nested message loop can't block the test thread) and polls for the modal. Searches desktop children AND MainWindow's owned descendants (WPF's MessageBox is an HWND-owned child of MainWindow, not a top-level window). ✅ ModalTests
Find a modal (non-blocking) TestApp.FindModalWindow(titleSubstring) ✅ ModalTests
Dismiss modal by button name (text) TestApp.InvokeButtonByName(modal, "Yes") ✅ ModalTests
Dismiss modal by AutomationId TestApp.InvokeButtonById(modal, "DialogOkButton") ✅ ModalTests
Assert modal's message body contains a substring TestApp.AssertContainsText(modal, "Proceed") / ContainsText(...) returns bool ✅ ModalTests
Assert modal has specific buttons (by Name or AutomationId) TestApp.HasButton(modal, "Yes") / HasButton(modal, "DialogOkButton") ✅ ModalTests
Diagnose "why didn't my modal show up?" TestApp.DescribeAllTopLevelWindows() returns a printable list 🔧
Auto-dismiss unexpected modals + record what appeared using var watchdog = app.StartModalWatchdog(); — background poller. Default button-press order is Cancel/No/Close/OK/Yes (safe defaults: prefer not to approve unknown dialogs). watchdog.DismissedCount and watchdog.DismissedTitles let tests assert "no unexpected modals" or "the expected error dialog fired". Custom shouldDismiss predicate lets tests ignore specific windows. ✅ ModalWatchdogTests
Drive a second non-modal top-level window (e.g. wizard step) TestApp.FindTopLevelWindowByTitle(...) helper 💭

Async commands & concurrency

Feature Kit capability Status
Run work off the UI thread from a VM command IBackgroundJobService.RunAsync(...) (Task.Run wrapper) ✅ SlowJobTests
Unit-test async commands synchronously InlineBackgroundJobService (returns Task.FromResult) 🔧
Observe VM state mid-flight in a unit test GatedBackgroundJobService (blocks on TaskCompletionSource until Release()) 🔧
Progress reporting through IProgress<double> RunAsync overload with IProgress<double> 🔧
Serialize work triggered from multiple sources (UI + CLI + script) R1.c via BatchJobRunner + ChannelBatchJobQueue + IBatchJobDispatcher ✅ BatchTests
Run a batch of jobs without managing the queue yourself BatchJobRunner.RunAsync(IEnumerable<BatchJob>) — builds a fresh channel internally each call ✅ BatchTests
Single-UI-control reentry guard R1.aAsyncRelayCommand.IsRunning + CanExecute from CommunityToolkit.Mvvm (not the kit's code) companion
Per-instance lock around native/shared state R1.b — app-specific; kit does not ship a base class not owned

Failure observability

Feature Kit capability Status
DispatcherUnhandledException → trace DiagnosticsExceptionSinks.AttachTo(app, diag) returns IDisposable (unsubscribes all 3 handlers on Dispose) ✅ ExceptionSinkTests (ThrowSync)
async void throw after await → trace Same sink (SyncContext marshals to dispatcher) ✅ ExceptionSinkTests (ThrowAfterAwait)
TaskScheduler.UnobservedTaskException → trace Sink, also sets SetObserved() 🔧
AppDomain.UnhandledException → trace Sink, best-effort 🔧
Thread-safe ring-buffer trace log (8192 entries) DiagnosticsService 🔧 DiagnosticsServiceTests
Error-count accounting with buffer eviction Evict oldest error → decrement count 🔧 DiagnosticsServiceTests
Lazy-cached FullText / Entries (don't rebuild on every read) Cache invalidates on write; rebuild on next read only 🔧
INotifyPropertyChanged for live UIA updates ObservableObject base raises FullText / ErrorCount on every write 🔧
Read trace/error-count from a running process TestApp.GetDiagnosticsTrace() + GetDiagnosticsErrorCount() ✅ ExceptionSinkTests
Fail a test if any error was logged TestApp.AssertNoDiagnosticsErrors(writeLine) ✅ CounterTests, SlowJobTests, SecondaryTabTests, ReachabilityTests
Throw if diagnostics UserControl is missing from the window TestApp.VerifyDiagnosticsSurface() / probe inside AssertNoDiagnosticsErrors 🔧
WPF data-binding errors → trace Hook PresentationTraceSources.DataBindingSource into IDiagnosticsService 💭
XAML parser warnings → trace Similar hook, if feasible 💭

Reachability & layout (NEW — the point of WpfTestKit that nobody else solves)

Feature Kit capability Status
Element present in UIA tree, not clipped, not zero-sized TestApp.CheckReachability(id)Reachability record, AssertReachable(id) ✅ ReachabilityTests
Detect element outside window bounds (no ScrollViewer → off-canvas) ReachabilityStatus.OutsideWindow ✅ ReachabilityTests
Detect element partially clipped by window edge ReachabilityStatus.PartiallyClipped ✅ ReachabilityTests
Detect element with zero width or height ReachabilityStatus.ZeroSized 🔧
Honor UIA's own IsOffscreen property ReachabilityStatus.OffscreenViaUia 🔧
Resize the main window from a test TestApp.ResizeMainWindow(w, h) via Transform pattern ✅ ReachabilityTests
Detect element covered by another UIA-visible element (z-order) ReachabilityStatus.HitTestBlockedAutomation.FromPoint(center) returns something other than the target or its descendant; reason string names the blocking element ✅ VisibilityReachabilityTests
Detect Visibility=Hidden ReachabilityStatus.NotFound — WPF removes Visibility=Hidden elements from the UIA tree entirely ✅ VisibilityReachabilityTests
Detect Opacity=0 / IsHitTestVisible=false Known limitation — UIA treats both as hit-testable from out-of-process. Pinned as documented behavior in VisibilityReachabilityTests. Consumers who care about visual-opacity must validate via screenshots. ⚠️ documented
Detect element behind a ScrollViewer not scrolled into view ReachabilityStatus.ScrolledOutOfView when an ancestor has ScrollPattern and the element is outside the client area 🔧
Compare against window client area (not outer rect with chrome) GetClientRect + ClientToScreen P/Invoke, fall back to outer rect on failure 🔧
Audit every AutomationId'd element for reachability at a given size TestApp.AuditReachability() → list of failures 💭
Audit every interactive element has an AutomationId Scan UIA tree for clickable/editable elements missing IDs 💭

OS file dialogs (preferred: stub via DI; fallback: watchdog)

Feature Kit capability Status
Abstraction for OS file-picker dialogs IFileDialogServicePickFileToOpen / PickPathToSave 🔧
Production implementation wrapping WPF/Win32 dialogs FileDialogService (uses Microsoft.Win32.OpenFileDialog / SaveFileDialog) 🔧
Test-mode stub with queued results + call recording StubFileDialogServiceQueueOpenResult(path), QueueSaveResult(path), OpenCalls / SaveCalls history ✅ FileDialogTests
DI registration for production services.AddWpfTestKitFileDialogs() 🔧
Consumer-side startup-arg swap (test → stub) Pattern shown in WpfDemo/App.xaml.cs: --test-file-to-open=<path> (or CANCEL) triggers the stub swap before BuildServiceProvider. Consumer owns the flag name and wiring. ✅ FileDialogTests
Fallback for tests that can't stub: auto-dismiss the real shell dialog StartModalWatchdog() detects the OS dialog as a foreign window and clicks Cancel. Fragile — shell UIA tree varies across Windows versions. Use only when stubbing is impossible. ⚠️ FileDialogTests (the fallback test is tagged as known-flaky)

Keyboard & input

Feature Kit capability Status
Press a keyboard shortcut (Ctrl+S, Ctrl+Shift+E) TestApp.PressShortcut("Ctrl+Shift+E") — parses string, calls Keyboard.TypeSimultaneously. Requires window foreground (handled via EnsureForeground). ✅ KeyboardTests
Press a single key (Enter, Escape, F2) TestApp.PressKey("Enter") — supports A-Z, 0-9, F1-F24, arrows, Enter, Escape, Tab, Space, Backspace, Delete, Home, End 🔧
Press a chord sequence (Ctrl+K, Ctrl+S / Alt+F, N) TestApp.PressChord("Ctrl+K", "Ctrl+E") — fires each combo in sequence with an 80ms settle between. Single-key elements are supported so PressChord("Alt+F", "N") works for menu mnemonics. ✅ KeyboardTests (chord + mnemonic)
Access keys / underlined mnemonics (_File) Same PressChord helper — PressChord("Alt+F", "N") opens File menu then invokes _New ✅ KeyboardTests
Type plaintext into currently focused element via real keystrokes TestApp.SendKeys("text") wraps FlaUI's Keyboard.Type. Use this when you need to exercise real KeyDown handlers — for just setting a TextBox's value, prefer TypeInto (ValuePattern, no focus required). ✅ KeyboardTests
Bring window to foreground without stealing inner focus TestApp.EnsureForeground() — calls SetForeground only; does NOT call Focus() (which would override an earlier element.Focus call) 🔧
Assert focus is on a specific element TestApp.AssertFocused(id) / GetFocusedAutomationId() ✅ KeyboardTests
Tab-order traversal to verify focus chain TestApp.PressKey("Tab") + AssertFocused — compose as needed 🔧
Drag-and-drop Synthesized mouse move/down/up sequence 💭

Data editing

Feature Kit capability Status
Set a DataGrid cell value by header + row Playbook keystroke pattern (F2 → clear → type → Tab) 🚧
Read a DataGrid cell value TestApp.GetDataGridCellValue(...) 🚧
Filter/scroll DataGrid before editing (virtualization trap) Not ours to own; consumer drives; kit can offer row-wait helper 💭

Evidence capture on failure

Feature Kit capability Status
Screenshot the main window to PNG ScreenshotHelper.CaptureToFile, TestApp.TakeScreenshot(label) 🔧
Dump UIA subtree to text UiaTreeDumper.DumpToFile, TestApp.DumpUiaTree(label) 🔧
Auto-capture both on test dispose FlaUiTestFixture base class (in WpfTestKit.Xunit — separate assembly so the kit itself has no xUnit dep) ✅ all WpfDemo tests
Capture-on-failure policy (opt-in to capture only when assertions fail) ArtifactCapturePolicy.OnFailure (default) / Always / Never — uses AppDomain.FirstChanceException + Xunit.Sdk.XunitException detection (by name, no xunit-core dep) 🔧
Pixel-diff visual regression vs a baseline PNG TestApp.AssertScreenshotMatches(baselineName, baselineDir, diffTolerancePct, perPixelTolerance) — first run saves baseline and throws "baseline established"; subsequent runs compare and write a diff image to temp on failure ✅ ScreenshotBaselineTests
Low-level pixel diff primitive ImageDiff.Compare(baseline, actual, perPixelTolerance)DiffResult(DiffPercentage, DifferentPixels, TotalPixels, DiffImage) 🔧 ImageDiffTests
Record a trace of test steps with screenshots + HTML report var trace = new TraceRecorder(); trace.Record("label", () => { ... }); trace.WriteHtmlReport("title", path) — self-contained HTML with base64-embedded screenshots 🔧 TraceRecorderTests
WCAG contrast check on visible text Sample FG/BG per TextBlock, compute ratio 💭

Debugging aids (mid-test inspection)

Feature Kit capability Status
Pause mid-test for human inspection app.Pause("why") opens a small floating "Resume" window on the STA thread and blocks the test until click or timeout. Set env var WPFTESTKIT_HEADLESS=1 in CI to make it a no-op — safe to leave Pause() calls in tests. ✅ PauseTests
Time-travel trace report (Playwright-style) TraceRecorder wraps test actions with timing + optional screenshots; WriteHtmlReport produces a self-contained HTML 🔧 TraceRecorderTests

Integration wiring

Feature Kit capability Status
Register diagnostics + background-job as singletons services.AddWpfTestKitDiagnostics() 🔧 ServiceCollectionExtensionsTests
Register batch queue + runner + consumer dispatcher services.AddWpfTestKitBatch<TDispatcher>() 🔧 ServiceCollectionExtensionsTests
Wire all three global exception sinks in one line DiagnosticsExceptionSinks.AttachTo(this, diagnostics) ✅ (implicit in every WpfDemo test that asserts zero errors)
Publish diagnostics to UIA via drop-in UserControl <diagnostics:DiagnosticsUiaSurface /> ✅ (used by WpfDemo)
Override UIA surface on a per-instance basis (rare) DiagnosticsUiaSurface.Diagnostics property 🔧
Parse --flag value startup args for test-mode swaps string[].ExtractFlagValue(name) 🔧
Check presence of a boolean switch in startup args string[].HasFlag(name) 🔧 StartupArgsExtensionsTests
Launch the consumer .exe and locate its main window TestApp.Launch(exePath, args, timeoutMs) ✅ (every WpfDemo test)
Locate the WPF project's .exe from a sibling test project TestApp.LocateExeRelativeToSolution("WpfDemo") — walks up, probes bin/**/WpfDemo.exe, picks most recently built ✅ (WpfDemoTestApp uses it)

Resource hygiene & teardown

Feature Kit capability Status
Clean app shutdown on test teardown FlaUiTestFixture.Dispose disposes TestApp ✅ (every WpfDemo test)
TestApp.Dispose closes app + disposes automation Application.Close() + Application.Dispose() + UIA3Automation.Dispose() 🔧
Detect leaked WPF window references after teardown WeakReference + GC.Collect + final assertion 💭
Detect processes surviving teardown Process-tree cleanup assertion 💭

Audits & hardening (no consumer trigger; run once per suite)

Feature Kit capability Status
All interactive UIA elements have an AutomationId app.AuditAutomationIds().MissingOnInteractiveElements — scans the subtree for Button / CheckBox / ComboBox / Edit / MenuItem / RadioButton / etc. that lack an AutomationId. Each entry includes ControlType, Name, and bounds. ✅ AutomationIdAuditTests
All AutomationIds are unique app.AuditAutomationIds().DuplicateIds — reports each ID used by more than one element with all occurrences ✅ AutomationIdAuditTests
Single "is my audit clean" check app.AuditAutomationIds().IsClean ✅ AutomationIdAuditTests
All AutomationIds referenced by tests exist Static analysis or runtime "this test mentioned FooButton but tree has no such id" warning 💭
Tests don't leak UIA handles Track FindFirstDescendant results across tests 💭
Retry decorator for known-flaky scenarios TestApp.Retry(action, maxAttempts, backoff) 💭

Consumer-side wiring (App.xaml.cs)

public partial class App : Application
{
    private IServiceProvider? _services;

    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        var services = new ServiceCollection();
        services.AddWpfTestKitDiagnostics();
        services.AddWpfTestKitFileDialogs();
        services.AddSingleton<MainViewModel>();
        _services = services.BuildServiceProvider();

        var diagnostics = _services.GetRequiredService<IDiagnosticsService>();
        DiagnosticsExceptionSinks.AttachTo(this, diagnostics);
        DiagnosticsUiaSurface.DefaultDiagnostics = diagnostics;

        new MainWindow(_services.GetRequiredService<MainViewModel>()).Show();
    }
}

And in MainWindow.xaml:

<Window xmlns:diagnostics="clr-namespace:WpfTestKit.Diagnostics;assembly=WpfTestKit" ...>
    <Grid>
        
        <diagnostics:DiagnosticsUiaSurface />
    </Grid>
</Window>

That's enough for FlaUI tests to find the diagnostics surface and assert on errors.

Product Compatible and additional computed target framework versions.
.NET net8.0-windows7.0 is compatible.  net9.0-windows was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.1 96 5/10/2026