winwright drives a Windows desktop application from a test and reports what was actually observed. It answers in four verdicts rather than two, so a check whose precondition was absent is named in the summary instead of disappearing from the count.
winwright run cases/report.wwx --case language-round-tripdesk this run's alone no other session is attached binary bin/Debug/App.exe built 2m ago, from the tree in hand language pt-BR settings.json -> ui.language foreground granted App — Relatórios spectators none nothing else is showing this application store fingerprinted %APPDATA%/App, compared again at the end
resolve Window#main > Pane#reportHost > Text[order=top]observedact invoke MenuItem[key=menu.language.ptBR]observedassert every label matches the app's own pt-BR stringsobservedcapture report pane -> artifacts/report.pt-BR.pngnot observedverify the machine is as this run found itobservedThe last line is the product. An assertion whose precondition was absent did not pass and did not fail — it never ran, it is named in the summary, and collapsing it into either of the other two is the thing winwright will not do. The exit code is 2, so CI can tell the difference without reading a word.
net10.0-windowsOne locator grammar, read the same by every verbPattern acts need no foregroundTwo packages, and an app takes at most oneA mapping written twice is a mapping that drifts, and CI reads the number rather than the word — so the enum's values are the process exit codes rather than being translated into them. 2 is the reason this project exists.
Every assertion ran and every one of them held.
And it means the roll call agreed: a run short of what discovery found is not reported as a pass, because that is exactly the shape the original defect had.
At least one assertion ran and did not hold.
One assertion ran and did not hold. The failing step carries the view the diagnosis built — the tree as it was, the element's facts, what its patterns read — so nobody writes a throwaway script to see what the window had.
Everything that ran passed, and something could not be evaluated at all.
The one this project exists for. Each unevaluated reading is named in the summary by name; three of the ones a scenario meets often are about the desk rather than about your application, and none of them is your code being wrong.
The harness broke — something threw, and what it says is about this tool rather than about the application under test.
It outranks the rest, because a reader told the build failed opens the wrong repository. What the message says is about this tool, and nothing after the throw was observed at all.
winwright — cases/report.wwx
preamble
desk this run's alone nothing else is attached
binary bin/Debug/App.exe built 2m ago, from the tree in hand
staleness current sources unchanged since the build
language pt-BR settings.json -> ui.language
foreground granted App — Relatorios
spectators none nothing else is showing this application
display not read the application does not take Winwright.InApp
assertions
[held] report.title.pt-BR 27 labels derived, 27 matched
[held] report.totals.rounding derived from the app's own read-out
[held] store.unchanged fingerprint equal, before and after
[----] capture.report not observed — the shell would not open the
overflow flyout, so the pane could not be
brought to the front
Degraded (2). 3 assertions ran and held. 1 was never evaluated, and it is named above.Before the assertions, a run takes one reading of the machine: the desk it is on, which binary it is driving, whether that binary is stale, the resolved language, the foreground, the launch arguments, whether anything else is showing the application, and whether the desk is this run's alone. Each is reported as measured, absent, or not read — an absent line and a missing line read the same to somebody skimming, and only one of them is a statement.
Binding, in the same sense as the product's own. A feature that breaks one is wrong even if it was asked for, and each names the defect it prevents rather than the value it expresses.
The whole project follows from one measurement: a suite reported a pass with a total of 352 where the run before it had 374. Twenty-two checks were gone and the only sign was a number nobody had a reason to read.
Absent and not read are different facts and they read the same to somebody skimming. Every reading is reported as measured, absent, or not taken — and only one of those three is a statement about your application.
A foreground Windows would not grant, a focus that left mid-poll, a flyout the shell would not open. None of them is your code being wrong, so none of them goes red — the answer names what the desk did instead.
Every verb reads the same locator string the same way, and the combinator means a descendant of, at any depth — because UI Automation wraps controls in panes that differ between frameworks and between a maximised window and a restored one.
An act asks the control through its own accessibility peer, which needs no foreground and no mouse. The verbs that do synthesise input are marked as such in the catalogue rather than discovered on a red run.
An expected label read from the application's own language file cannot drift from it. A string retyped into a test is a second copy of the truth, and the second copy is the one that goes stale.
A capture that a window covered, that a backdrop transmitted through, that came back one flat colour, or that photographed a page still computing is refused — and the refusal names the intruder, its process and the rectangle.
A locator that does not parse names the position and the reason. A misspelt control type is answered with the nearest name UI Automation actually has. An error that costs a round trip to understand is a defect.
The store a case writes through is fingerprinted before and compared after, so the promise that a run changed nothing of yours is asserted where it is most likely to be broken rather than stated in a readme.
There is no recorder that turns clicks into a scenario, and no assertion invented on your behalf. What the engine owns is the loop, the waits and the verdicts; what you own is what is worth asserting.
A locator is a string, it parses or it is refused, and the refusal names the position and the reason. Locator.TryParse answers without throwing, for a caller collecting refusals rather than stopping at the first.
#saveButtonthe automation idButtonthe control typeButton#saveButtonboth at onceButton[name="Save as..."]the namePane[class=Chrome_WidgetWin_1]the window classButton[pattern=Invoke]it must carry that patternText[name="Statistics"][order=left]the leftmost of the ones that matchMenuItem[order=top][index=2]the second from the topWindow#main > Pane > Button#savea descendant of, at any depthIt asks the control through its own accessibility peer rather than asking the desktop to move a mouse. So a run does not need the screen to itself, and an act does not fail because something else took the foreground while it was polling.
Invoke, toggle, set a value or a range, select, expand, collapse. Each act is attempted to a cap and counted, so attempt 3 of 3 is in the record rather than in somebody's memory of the run.
Whether an element can take an act at all — and where it cannot, why not. A preflight takes every declared act and checks what it needs against the tree before anything is pressed.
Synthesised mouse input is there for the controls that have no peer worth asking. Every verb that needs the foreground is marked as such in the catalogue, and the declared reading says why the act could not be done any other way.
Synthesised keys, traversal keys at a window, and a focus read against the application under test rather than against the whole desk — because “something else has the focus” and “your window lost it” are different facts.
Enter a menu bar, walk to an entry, open a submenu, dismiss. The walk reports its hop count, which is what makes a timing observation about a picker mean anything.
The tray, the overflow flyout, the icons on either, and an icon's context menu. Finding an icon says which of the two happened — the icon is not there, or the flyout would not open — because a caller reading a null cannot tell them apart.
A declared destructive entry reached without saying you meant it is refused. destructive is the one project key with a refusal of its own: a bare name is rejected where the project ships more than one language, because a name is the field a translation rewrites — and a safety check compared against text a person sees has an expiry date.
A string retyped into a test is a second copy of the truth, and the second copy is the one that goes stale. So the expected set is read from what the application itself declares — its language files, its layout, its own read-out — and the comparison is against that.
A screenshot is evidence only if something rules out the ways it can lie. Each of these was a real session that exited zero with a file nobody could use, so each is a refusal rather than a warning.
The z order above the window is enumerated and every frame intersected with the copy rectangle, which answers for the whole area in one pass — sampled points cannot — and names the intruder, its process and the rectangle it covers.
A copy trimmed around an intruder is a picture of something nobody asked for. So the refusal is the answer, and it is actionable instead of mysterious.
A window with a system backdrop transmits what is behind it, and z-order reasoning cannot answer for that. A window that opted into one is refused rather than merely warned about.
A copy of exactly one colour is what a session where nothing was rendering produced — and it was written, reported as a capture, and exited zero.
The loading strings are read from the project's own language files before anything launches, so a key none of them carries refuses the run instead of matching nothing.
Which is what makes a difference a real difference, and means nobody has to choose a tolerance for a comparison to mean anything.
A render is measured, arranged and updated in that order. The arrange is why the verb exists: a tree that was measured and never arranged renders as a fully transparent picture of exactly the right size, which looks like a drawing bug and is a calling bug.
Steps, locators, acts and expectations are fields. The loop, the waits and the verdicts are the engine's. What is left in the file is the part that is actually about your application — including the defect the case exists to catch.
# the defect this case exists to catch — a case that can name none is one # nobody can justify, and one removed by accident is one nobody misses case: language-round-trip catches: a translated menu entry that leaves the report pane's labels in English precondition: language: pt-BR # absent -> named as unchecked, never red fixture: app: bin/Debug/App.exe environment: [ light, dark ] # every launch this case makes, not only the first shareable: false # this one writes, so it owns its process steps: - resolve: Window#main > Pane#reportHost > Text[order=top] - act: invoke MenuItem[key=menu.language.ptBR] - assert: labels from strings.pt-BR.json - capture: Pane#reportHost -> artifacts/report.pt-BR.png
winwright.json, found by walking up from where a run startsEvery key is optional. A reading that needs one this file does not declare is recorded as not taken, never quietly skipped — which is the same rule as everything else here, applied to configuration.
{
"executable": "bin/Debug/net10.0-windows/YourApp.exe",
"sourceRoot": "src/YourApp",
"sourceIgnore": ["bin", "obj"],
"fingerprintStore": "%APPDATA%/YourApp",
"languageFiles": ["strings.en.json", "strings.pt-BR.json"],
"language": { "preferenceFile": "settings.json", "preferenceKey": "ui.language", "fallback": "en" },
"timeouts": { "resolve": 5000, "stop": 5000 },
"attempts": 3,
"destructive": [{ "id": "quitCommand" }, { "key": "menu.exit" }]
}destructive names the entries that end the run. Write {"id": …} or {"key": …} rather than a bare name where the project ships more than one language: a name is the field a translation rewrites, and a safety check compared against text a person sees expires the moment somebody translates the application.
Two commands in the repository wire every clone, and nothing is added to any path. The tools carry this project's scenario schema as their input schema, so a case an agent writes is corrected at insertion rather than at run time; the skill loads when a window is in play rather than on every turn; and a hook denies a hand-written harness script and names the verb that replaces it.
One entry in .claude/settings.json:
Bash(winwright read:*)read is a promise, not a naming convention: a verb under it that drives the application is a defect. So this single grant removes every prompt on the inspection path — the tree, the facts, the verdict of the last run — while everything that presses a control still asks.
Every reading and every pattern act so far runs against an application that references nothing. What the in-app half adds is the handful of readings a harness cannot take from outside the process — and it references the engine not at all, so nothing here ships a test harness to your users.
WinwrightCaptureBackground, or the window's own. The system palette is not consulted at all: it answers white on a machine whose window is dark.The harness half is referenced by the project that drives the application. The in-app half is optional, and deliberately so: every reading and every pattern act runs against an application that references nothing. What the second package buys is the handful of readings a harness cannot take from outside the process.
Locate, act, assert, capture, and assemble the verdict. Referenced by whoever drives the application — never by the application.
Coordinates, render, backgrounds, geometry, popups — the readings that can only be taken from inside. It references the engine not at all, so nothing here ships a test harness to your users.
An application shipped to its users reports nothing and writes no file, which is what makes the in-app protocol safe to leave in a release.
Each of these is the argument for one part of the tool, at the length the argument actually takes. The summaries above are the same claims, shorter.
A run ends in one of four outcomes whose enum values are the process exit codes. Degraded (2) is what an assertion that could not be evaluated produces, named rather than dropped.
Read it →A locator is a string that parses or is refused with the position and the reason. The combinator means a descendant of, at any depth, because the panes UI Automation inserts differ between frameworks and between a maximised window and a restored one.
Read it →Invoke, toggle, set a value or a range, select, expand, collapse — through the control's own patterns, which needs no foreground. The verbs that synthesise input are marked as such in the catalogue rather than discovered on a red run.
Read it →Expectations are read from what the application itself declares — its language files, its layout, its own read-out — so switching the resolved language switches the expectation with it and a retyped string cannot go stale in silence.
Read it →Every way a screenshot can lie is a refusal: something over the rectangle, a system backdrop transmitting through the glass, one flat colour, a page still computing, or a window this run is not driving.
Read it →Steps, locators, acts and expectations are fields; the loop, the waits and the verdicts belong to the engine. Every field is validated at insertion, so a refusal costs a retry and never a deletion.
Read it →An optional second package for the readings a harness cannot take from outside the process: coordinates, render, backgrounds, geometry, popups. It references the engine not at all, so nothing here ships a test harness to your users.
Read it →Five things this project has decided against, written down where they can be pointed at. A tool with no stated non-goals is a tool that will eventually be asked for all of them.
The whole engine is UI Automation and Win32. Portability would mean a different tool wearing the same name, and it is not coming.
A package in the engine is a package every adopting project inherits. The two halves are separate assemblies precisely so an application takes the one it needs without inheriting the other.
A picture is evidence that something was drawn and that nothing was over it. What a pixel is worth arguing about belongs to a design review, not to a suite.
A recorded case asserts what happened rather than what matters, and nobody can tell the two apart afterwards. The tool never writes the test.
A run is a process that starts, observes, reports and stops. Nothing here outlives it, and nothing here is installed on the machine that hosts it.
The harness half goes in the project that drives the application. The in-app half goes in the application, and only if you want the readings that can only be taken from inside — every verb on this page works without it.
the test project
<PackageReference Include="Winwright" Version="0.1.0" />the application under test, and only if you want it
<PackageReference Include="Winwright.InApp" Version="0.1.0" />The suite that proves this creates real windows, takes the foreground and synthesises input, which is why it ships with a way to run inside a virtual machine so the host stays usable. A bare dotnet test takes the roll call too: a run short of what discovery found is not reported as a pass.