WpfUiTestServer 1.3.0

dotnet add package WpfUiTestServer --version 1.3.0
                    
NuGet\Install-Package WpfUiTestServer -Version 1.3.0
                    
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="WpfUiTestServer" Version="1.3.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="WpfUiTestServer" Version="1.3.0" />
                    
Directory.Packages.props
<PackageReference Include="WpfUiTestServer" />
                    
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 WpfUiTestServer --version 1.3.0
                    
#r "nuget: WpfUiTestServer, 1.3.0"
                    
#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 WpfUiTestServer@1.3.0
                    
#: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=WpfUiTestServer&version=1.3.0
                    
Install as a Cake Addin
#tool nuget:?package=WpfUiTestServer&version=1.3.0
                    
Install as a Cake Tool

WpfUiTestServer

A flag-gated, in-process HTTP server that lets an external script drive a WPF application's UI and read its state back — so a change can be exercised end to end (and self-verified) instead of always being handed to a human to click.

It is app-agnostic: the engine knows how to address and drive any WPF UI by x:Uid via AutomationPeers. Everything app-specific (domain state, message-box routing) enters through small seams the host supplies. It has no external dependencies and adds no reference cycle.

  • Transport: a raw TcpListener bound to 127.0.0.1 (loopback) — not HttpListener, which would need a netsh http add urlacl reservation or admin. Loopback TCP needs no privilege, so it works unattended.
  • Security: listens on loopback only, and only when the host explicitly enables it. A normal run never opens a socket. There is no authentication — anything that can reach loopback can drive the app, so enable it only in test/dev.
  • Addressing: WPF surfaces x:Uid at runtime as UIElement.Uid. The server walks the visual tree of every open window and acts through each element's AutomationPeer — the same machinery real UI-automation uses, so it respects IsEnabled and fires the real handlers regardless of window position.

Embedding it in a host app

1. Reference the assembly

Add a project/assembly reference to WpfUiTestServer. Because it has no dependencies, any project can reference it without a cycle.

2. Start it once the UI is built

Call Start after the main window's tabs/views are realized (e.g. at the end of your startup routine), gated behind a flag/env var so it never runs in a normal session:

if (testServerEnabled)
    WpfUiTestServer.UiTestServer.Start(
        mainWindow,                       // the window whose visual tree is addressed
        port,                             // <= 0 uses DefaultPort (8760)
        new MyStatusProvider(mainWindow), // optional IUiTestStatusProvider (null => /status empty)
        msg => MyLog.Write(msg));         // optional log sink (null => no-op)

A gently-pulsing "UNDER TEST-SERVER CONTROL" banner is docked across the top of the window while it runs, so an operator watching the machine knows automated input is live.

3. Supply domain state (optional) — IUiTestStatusProvider

State a test asserts on but that no single control exposes (connection, mode, progress, …):

public IEnumerable<KeyValuePair<string,string>> GetStatus()
{
    yield return new KeyValuePair<string,string>("connected", IsConnected ? "true" : "false");
    yield return new KeyValuePair<string,string>("state", ControllerState.ToString());
    // ...
}

Names become the /status keys and the /waitfor?status= query keys (matched case-insensitively). GetStatus is called on the UI thread, so it may read view-models directly.

4. Route message boxes through the dialog seam (optional)

So the harness can answer/read the app's prompts instead of a modal blocking an unattended run, funnel your MessageBox.Show calls through a wrapper that consults UiTestServer.Prompt and otherwise shows the real box. See CNC.Core.AppDialogs in ioSender for the reference wrapper. Prompt returns null when the server is off, so real users are unaffected.

Addressing generated controls. x:Uid is a XAML markup directive; controls created in code have none. Set UIElement.Uid explicitly on them (e.g. from a registry key) if you want them addressable.


HTTP API

All responses are JSON with an "ok" boolean. {uid} is a path segment; other params are query string. Duplicate x:Uids (a reused template) are disambiguated with ?index=N (0-based, in visual-tree order).

Discovery & state

Method & path Purpose
GET /ping Liveness → {"ok":true,"server":"…","port":8760}
GET /uids Distinct realized uids (sorted) with type and occurrence count — the "what can I address" list
GET /tree Every realized element carrying an x:Uid, with index,type,name,enabled,visible,value
GET /state/{uid}?index=N One element's full state
GET /screenshot PNG (image/png) of the whole window — lets the harness see rendered layout
GET /screenshot/{uid}?index=N PNG of a single element's bounds

Actions

Method & path Purpose
POST /invoke/{uid}?index=N Invoke (button), else Toggle, else Select (tab/list item) via the automation peer
POST /set/{uid}?value=V&index=N Set value: ValuePattern text, a checkbox/toggle bool, or a range number
POST /select/{uid}?itemIndex=N Select a DataGrid row / ComboBox / ListBox item by index — bypasses AutomationPeer (rows/items aren't realized as walkable peers until scrolled/dropped open) by setting Selector.SelectedIndex directly
POST /select/{uid}?text=T …or by display text (the DisplayMemberPath property value if set, else item Content/ToString()), case-insensitive
POST /key/{keyName}?uid=T Raise a key (real routed events) on target T (default = the window). Plain keys only — synthesized events can't set Keyboard.Modifiers, so Ctrl/Shift/Alt combos won't fire handlers. Use e.g. F1, Escape, Up
POST /menu/{uid} Open the element's context menu (fires its Opened, so dynamic submenus populate); returns the items (uid,header,enabled,hasItems)
POST /menu/{uid}?item=X …and invoke the menu item with uid X

Synchronization

Method & path Purpose
GET /idle Block until the Dispatcher drains to Background priority (layout/arrange done) and an actual composed frame has rendered (bounded 1s wait - a no-op if nothing changed). Does not know about async I/O
GET /status Host/domain state map from the IUiTestStatusProvider
GET /waitfor?… Block until a condition holds or timeout (ms, default 5000) elapses; polls every poll ms (default 100). Returns matched+elapsedMs, or timeout+last observed value
GET /exceptions[?since=N][?clear=true] Unhandled exceptions the app fed via RecordException, plus any route-caught throw — newest first (seq,when,source,type,message,stack). since returns only seq > N (polling); clear empties the buffer

Exceptions. A synchronous throw from a driven action is returned inline ({"ok":false,"error":"route threw: …"}, HTTP 500) and logged to /exceptions. For unhandled exceptions the host must call UiTestServer.RecordException(source, ex) from its global handlers; a host may also choose to keep the app alive in test mode (ioSender continues on a Dispatcher exception when the server is on) so the harness can read /exceptions and carry on rather than seeing the socket drop.

/waitfor conditions (pick one):

  • ?uid=X&exists=true — element appears / false = goes away
  • ?uid=X&enabled=true · ?uid=X&visible=true
  • ?uid=X&value=Foo
  • ?status=state&equals=Idle — an IUiTestStatusProvider field

Dialog broker

The app's message boxes (routed through the host's wrapper → UiTestServer.Prompt) become answerable by the harness. These routes run without the Dispatcher, so they work even while the UI thread is blocked in a prompt (answer it on a second connection).

Method & path Purpose
POST /dialog/arm?answer=Yes Pre-answer the next prompt once (no modal appears)
POST /dialog/arm?standing=Yes Answer every prompt with this until cleared
POST /dialog/arm?capture=true Intercept prompts with no preset — they become pending, answered via /dialog
POST /dialog/arm?clear=true Clear armed queue + standing + capture
GET /dialogs capture/armed/standing, the pending list (id/title/message/buttons), and a recent ring buffer of shown prompts + how each resolved (readback of message-box output)
POST /dialog?answer=Yes[&id=X] Answer the oldest pending prompt (or the one with id)

When the server is running but nothing is armed and capture is off, Prompt returns null → the host shows its real dialog (so interactive use is unaffected). A captured prompt that is never answered resolves to the host's supplied default after its timeout.

Handoff

For an agent that has been driving the UI unattended up to a point where a human needs to take over - a physical machine action, a judgment call, anything outside what the harness can do.

Method & path Purpose
POST /handoff (body = plain-text instructions, optional) Release control: shows a non-modal popup with the instructions, removes the "under test-server control" banner, and stops the server

This is a real stop, not a pause — there is no resume-in-place. The listener closes and AcceptLoop exits; the app returns to completely normal manual operation with no automated input live. The popup is non-modal (Window.Show(), not a MessageBox) so the operator has full, immediate control of the app while reading it - it never blocks them from acting. If automation is needed again later in the same process, the host calls UiTestServer.Start(...) again (the same guard that made a second Start() a no-op while running now lets a fresh Start() succeed).

curl -s -X POST localhost:8760/handoff --data-raw "Jog to the corner and touch off Z manually, then run the probe from the Probing tab."

App exit

The server is in-process, so it can't announce its own death over HTTP — a request just fails once the app is gone. Two out-of-band channels cover exit:

  • Exit code — the harness owns the process, so it reads Process.ExitCode (0 = clean; ioSender uses 0xFA11/64017 as its crash sentinel).
  • Exit reason — the host may drop a small ioSender.exit.json ({code, crash, reason, when}) in its config dir on shutdown/crash, which the harness reads after the socket drops. (Crashes also leave the full crash log.)

Known limitations

  • Realized elements only. Content on a not-yet-selected tab is created lazily and won't appear until that tab has been shown once (POST /invoke/{tabUid} to realize it). The full static catalog of every declared x:Uid lives in the app's XAML/localization source, not at runtime.
  • /idle is dispatcher-only. It does not track async I/O (a connection, a running job). Use /waitfor on a status field or an element condition for those.
  • No auth, loopback only. By design; keep it test/dev-gated.

Example: assert an outcome

curl -s localhost:8760/waitfor?status=connected&equals=true&timeout=8000
curl -s -X POST localhost:8760/dialog/arm?standing=No       # decline any confirm
curl -s -X POST localhost:8760/invoke/btn_resetDefault      # would prompt; intercepted → No, no modal
curl -s localhost:8760/dialogs                               # read back what the box said (recent[0])
Product Compatible and additional computed target framework versions.
.NET Framework net462 is compatible.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETFramework 4.6.2

    • No dependencies.

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
1.3.0 214 7/14/2026
1.2.0 103 7/12/2026
1.1.0 118 7/12/2026
1.0.0 113 7/10/2026