Improve winapp ui --json output: stable envelopes, no leaks, no cycles#511
Conversation
Polishes the JSON output of every `winapp ui` subcommand so consumers get a
predictable, parseable shape and exit-code contract:
Envelope/shape fixes:
- `ui inspect --json`: nest elements under `windows[].elements[].children[]`
(was a flat list); strip per-element id/depth/parentSelector/windowHandle
(selectors are the public handle); preserve `hasMoreChildren` truncation hint
with a `+more` marker rendered in text mode.
- `ui inspect --interactive`: collapse non-interactive ancestors and surface
them as `ancestorPath` on surviving descendants; +more rendered for
truncated non-interactive subtrees in both text and JSON modes.
- `ui inspect --ancestors --json`: assign Depth=i to the ancestor chain so
BuildWindows nests it correctly (previously every ancestor became a sibling
root because Depth was unset).
- `ui get-focused --json`: emit `{ hasFocus, element? }` envelope (was a
bare `null` when nothing focused).
- `ui search` / `ui wait-for`: scrub internal id, parentSelector,
windowHandle from results (top-level + nested invokableAncestor).
Cycle prevention:
- New `UiElementScrubber` flattens InvokableAncestor to a hint
(selector/name/type only). System.Text.Json (no ReferenceHandler) would
otherwise throw on `inspect --interactive --json` when a surviving
descendant's invokableAncestor pointed back to one of its own tree ancestors.
Tests:
- 35/35 UiCommandTests pass (was 25). New coverage:
- Multi-window separator grouping.
- Depth-jump nesting (0->2).
- --ancestors JSON nesting (M3 regression).
- InvokableAncestor cycle (interactive + search).
- --json happy-path smoke for click/focus/set-value/scroll/scroll-into-view
so NativeAOT JSON registrations are exercised under Debug too.
Docs:
- Updated docs/ui-automation.md JSON examples to the new envelope shape.
- Documented the search/wait-for exit-code + envelope contract for --json mode
(envelope on stdout, exit 1, stderr empty when nothing matches / times out).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Build Metrics ReportBinary Sizes
Test Results✅ 842 passed, 1 skipped out of 843 tests in 577.2s (+17 tests, +160.9s vs. baseline) Test Coverage❌ 23.1% line coverage, 38.7% branch coverage · ✅ +1.6% vs. baseline CLI Startup Time34ms median (x64, Updated 2026-05-01 17:47:20 UTC · commit |
There was a problem hiding this comment.
Pull request overview
This PR standardizes and hardens the winapp ui --json output across subcommands by introducing stable result envelopes, scrubbing internal fields from serialized UiElements, and preventing reference cycles that can break System.Text.Json (especially under NativeAOT/trimming).
Changes:
- Reworked
ui inspect --jsonto emit a window-grouped nested element tree, added truncation hints (hasMoreChildren), and improved--interactivebehavior (ancestor collapsing + invokable-ancestor hints). - Added structured JSON error emission for
--jsonmode and updated UI commands to consistently honor the JSON contract (including timeout/no-match envelopes + exit codes). - Expanded test coverage to exercise new JSON shapes, edge cases (multi-window, depth jumps, ancestors nesting), and NativeAOT JSON context registrations.
Reviewed changes
Copilot reviewed 24 out of 24 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/winapp-CLI/WinApp.Cli/Services/UiAutomationService.cs | Enhances element walking with parent linkage, invokable detection, and truncation hints (HasMoreChildren). |
| src/winapp-CLI/WinApp.Cli/Models/UiElement.cs | Updates the UI element model for new JSON contracts (nullable id/depth/window handle, ancestor metadata, invokable flags). |
| src/winapp-CLI/WinApp.Cli/Helpers/UiJsonError.cs | New helper to emit structured JSON error envelopes to stderr in --json mode. |
| src/winapp-CLI/WinApp.Cli/Helpers/UiJsonContext.cs | Extends source-generated JSON context with new envelopes/types and higher max depth. |
| src/winapp-CLI/WinApp.Cli/Helpers/UiErrors.cs | Plumbs json through error helpers and emits JSON error envelopes when applicable. |
| src/winapp-CLI/WinApp.Cli/Helpers/UiElementScrubber.cs | New helper to scrub internal fields and flatten InvokableAncestor to prevent cycles in JSON. |
| src/winapp-CLI/WinApp.Cli/Commands/UiWaitForCommand.cs | Emits consistent JSON envelopes on success/timeout and emits JSON errors on failures/cancellation. |
| src/winapp-CLI/WinApp.Cli/Commands/UiStatusCommand.cs | Adds JSON-aware error handling for missing app and generic failures. |
| src/winapp-CLI/WinApp.Cli/Commands/UiSetValueCommand.cs | Adds JSON-aware errors and ensures element identifiers are safe when Id is null. |
| src/winapp-CLI/WinApp.Cli/Commands/UiSearchCommand.cs | Scrubs internal fields in JSON output and preserves exit-code behavior for no matches. |
| src/winapp-CLI/WinApp.Cli/Commands/UiScrollIntoViewCommand.cs | Adds JSON-aware errors and safe element identifier formatting. |
| src/winapp-CLI/WinApp.Cli/Commands/UiScrollCommand.cs | Adds JSON-aware argument errors and safe element identifier formatting. |
| src/winapp-CLI/WinApp.Cli/Commands/UiScreenshotCommand.cs | Adds per-window details for composite screenshots and JSON-aware error emission. |
| src/winapp-CLI/WinApp.Cli/Commands/UiListWindowsCommand.cs | Uses JSON-aware generic error handling. |
| src/winapp-CLI/WinApp.Cli/Commands/UiInvokeCommand.cs | Adds JSON-aware errors and safe element identifier formatting. |
| src/winapp-CLI/WinApp.Cli/Commands/UiInspectCommand.cs | Major rework of JSON output: stable envelope, per-window grouping, nesting, interactive collapsing, and cycle-safe ancestor hints. |
| src/winapp-CLI/WinApp.Cli/Commands/UiGetValueCommand.cs | Adds JSON-aware errors and safe element identifier formatting. |
| src/winapp-CLI/WinApp.Cli/Commands/UiGetPropertyCommand.cs | Adds JSON-aware errors and safe element identifier formatting. |
| src/winapp-CLI/WinApp.Cli/Commands/UiGetFocusedCommand.cs | Switches to a stable { hasFocus, element } JSON envelope and scrubs internal fields. |
| src/winapp-CLI/WinApp.Cli/Commands/UiFocusCommand.cs | Adds JSON-aware errors and safe element identifier formatting. |
| src/winapp-CLI/WinApp.Cli/Commands/UiClickCommand.cs | Adds JSON error emission for zero-size click targets and safe element identifier formatting. |
| src/winapp-CLI/WinApp.Cli.Tests/UiCommandTests.cs | Adds extensive coverage for new JSON shapes, edge cases, and NativeAOT registration smoke tests. |
| src/winapp-CLI/WinApp.Cli.Tests/FakeUiServices.cs | Makes focused-element behavior configurable for tests. |
| docs/ui-automation.md | Updates JSON examples and documents search/wait-for --json exit-code + envelope contract. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
CI release build promotes warnings to errors via TreatWarningsAsErrors. The inline `new Regex(...)` constructions used by StringAssert.DoesNotMatch in UiCommandTests are throwaway test literals where source-generated regex would add zero value, only file noise. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- UiElementScrubber: remarks listed 'depth' (not stripped) and omitted 'automationId' (which IS copied). Updated to match actual behavior. - UiElement: Depth/ParentSelector/AncestorPath docs claimed 'set on flat result lists (search)' but search/wait-for/get-focused do not populate them. Reworded to: populated by inspect; not populated by other commands. - FakeUiServices: removed unused FocusedResultSet flag. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PR #511 changed the inspect --json shape from { elements: [...] } to { depth, interactive, hideDisabled, hideOffscreen, windows: [{ elements: [...] }] }. Update the e2e test to navigate the new nested envelope. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Warning
Breaking change —
winapp uiJSON output shapes have changed. See "Breaking changes" below.Polishes the JSON output of every
winapp uisubcommand so consumers get a predictable, parseable shape and exit-code contract.Breaking changes
The following
--jsonenvelopes are not backward compatible. Update any scripts/tools parsing these outputs:ui inspect --json{ elements: [...] }(flat list){ depth, interactive, hideDisabled, hideOffscreen, windows: [{ hwnd, title, className, elementCount, elements: [{ ..., children: [...] }] }] }(nested per window)ui inspect --json(per element)id,depth,parentSelector,windowHandleselectoris the public handleui inspect --ancestors --jsonDepth=ichain (parent → child)ui get-focused --jsonnullwhen nothing focused{ hasFocus: false }(or{ hasFocus: true, element: {...} })ui search --json/ui wait-for --jsonid,parentSelector,windowHandle(top-level + nestedinvokableAncestor)Envelope/shape fixes
ui inspect --json: nest elements underwindows[].elements[].children[](was a flat list); strip per-elementid/depth/parentSelector/windowHandle(selectors are the public handle); preservehasMoreChildrentruncation hint with a+moremarker rendered in text mode.ui inspect --interactive: collapse non-interactive ancestors and surface them asancestorPathon surviving descendants;+morerendered for truncated non-interactive subtrees in both text and JSON modes.ui inspect --ancestors --json: assignDepth=ito the ancestor chain soBuildWindowsnests it correctly. Previously every ancestor became a sibling root becauseDepthwas unset.ui get-focused --json: emit{ hasFocus, element? }envelope (was a barenullwhen nothing focused — unparseable as a typed result).ui search/ui wait-for: scrub internalid,parentSelector,windowHandlefrom results (top-level + nestedinvokableAncestor).Cycle prevention
New
UiElementScrubberflattensInvokableAncestorto a hint (selector/name/type only).System.Text.Json(noReferenceHandler) would otherwise throw oninspect --interactive --jsonwhen a surviving descendant'sinvokableAncestorpointed back to one of its own tree ancestors.Tests
35/35
UiCommandTestspass (was 25). New coverage:0->2).--ancestors --jsonnesting regression.InvokableAncestorcycle (interactive + search paths).--jsonhappy-path smoke forclick/focus/set-value/scroll/scroll-into-viewso NativeAOT JSON registrations are exercised under Debug too (would otherwise only fail in published trim build).scripts/test-e2e-winui-ui.ps1to navigate the newwindows[].elements[]envelope.Docs
docs/ui-automation.mdJSON examples to the new envelope shape (.windows[0].elements[0]).search/wait-forexit-code + envelope contract for--jsonmode (envelope on stdout, exit 1, stderr empty when nothing matches / times out).Validation
scripts/build-cli.ps1 -SkipTests -SkipMsix -SkipNpmsucceeds (regenerates docs + skills + VSC + NuGet).