Repository navigation
Releases: alegauss/winwright
Release list
v1.1.5
This release fixes choose file on a Save dialog that offers a default name. Before, the case was
green while the application saved under the default name. One change is something an existing setup
may notice, and it is marked.
A Save dialog saves the name the case gave
{ "locator": "Button#ExportButton", "act": "invoke" },
{ "locator": "Window[class=\"#32770\"]", "act": "choose file", "with": "{files}/saved.png" },
{ "locator": "Window[class=\"#32770\"]", "act": "read", "absent": true, "named": "the dialog closed" },
{ "locator": "Text#ExportStatus", "act": "read", "reads": "name", "expect": "Saved saved.png" }An application that sets FileName on its SaveFileDialog gets a dialog that keeps that name
until its name box is edited. choose file put the path in the box as its value, and a value set
from outside does not count as an edit. The box read the path back, the dialog confirmed with its
default name anyway, and both choose file and the absent read passed. If a file with the default
name already existed, the dialog asked whether to replace it instead. That question is a second
#32770, so the dialog's own locator was refused as ambiguous.
choose file now types the path into the name box, as characters sent to the box's own window. The
dialog takes that as an edit and saves under the name the case gave. It still needs no foreground.
(May notice.) The box is read back before the dialog is confirmed. If it does not hold the
path, the step is refused, naming what the box held, and nothing is confirmed. Before, that case
was a green over the wrong file. The trace line names the act's route as WM_CHAR, Invoke where it
used to say Value, Invoke.
Installing
<PackageReference Include="Winwright" Version="1.1.5" />
<PackageReference Include="Winwright.InApp" Version="1.1.5" />Full Changelog: v1.1.4...v1.1.5
v1.1.4
This release lets a case move files in and out of the application: dropped onto it from outside,
and chosen through the common Open and Save dialogs. A window drawn by Direct3D can now be
photographed, a fixture can hand its staged directory to an application that takes paths as
flags, and an act no longer gives up on an element the application is still building. Two changes
are something an existing setup may notice, and they are marked.
A file dropped onto the application
{ "locator": "Pane#DropZone", "act": "drop file", "with": "{files}/photo.png", "named": "the photo is dropped" },
{ "locator": "Text#ImageSize", "act": "read", "reads": "name", "expect": "1920 x 1080" }drop file drags the file in with onto the step's element, the way Explorer would. with is
relative to the project's root, or in the fixture's staged directory through {files}. The drag is
a real system drag with the file list Explorer offers (CF_HDROP), so the application's own drop
handler runs. The effect the target answers is the act's evidence: a target that answers with no
effect is a hole that says so, not a pass. It needs the foreground and is attempted once. A file
that is not there stops the case.
A file chosen through the Open or Save dialog
{ "locator": "Button#ExportButton", "act": "invoke" },
{ "locator": "Window[class=\"#32770\"]", "act": "choose file", "with": "{files}/out" },
{ "locator": "Window[class=\"#32770\"]", "act": "read", "absent": true, "named": "the dialog closed" },
{ "file": "{files}/out.webp", "act": "read", "sameBytesAs": "cases/expected/out.webp" }choose file puts the path into the common dialog's name box and confirms, through the
controls' own patterns, so it needs no foreground. The locator is the dialog, #32770 by class.
Whether the dialog accepted the path is the next step's claim, not the act's: a read with
"absent": true says the dialog closed. Address the dialog's controls by id, never by name,
because the names are in the shell's language.
A Mica window with Direct3D content has a picture
A region drawn by a window of its own was a hole in the off-screen render: an HwndHost holding a
DXGI swapchain, a WinForms control or a browser is not in the visual tree. On a Mica window the
screen copy is refused for the glass as well, so such a window had no capture route at all.
The render now copies each such region from its window and draws it into the picture where the
tree reserved room for it. The copy asks the compositor for what it composed, so a Direct3D
swapchain is included and the glass around it is not. The receipt names each region it copied.
Three cases are refused instead of written:
- a hosted region in no window that is shown, which has nothing to copy from;
- a copy that comes back black throughout, which is what a surface the print could not reach
looks like; - a
D3DImagethat drew nothing into the render. A render is a software pass, and aD3DImage
reaches one only when its back buffer was set withenableSoftwareFallback: true.
A fixture's arguments can name the staged directory
{
"name": "exporting into its own folder",
"arguments": ["--open", "{files}/photo.jpg", "--export", "{files}/out.png", "--models", "{root}/models"],
"files": ["cases/fixtures/photo.jpg"]
}arguments now take the tokens a variable takes: {files}, {root} and {root/}. An application
whose paths come in as flags opens the staged copy and writes beside it, and its other paths still
resolve from the project's root. An argument naming {files} also counts for the check that refuses
files staged where nothing looks. Before, such a fixture was refused, and the token would have
reached the application literally.
An act waits for an element the application is still building
(May notice.) A select right after an expand on a WPF combo box failed about one run in
four, refused as "nothing matched". The look had found the list item, but the item's automation
peer was replaced before it could be read, and the act gave up on that look instead of polling on.
A look whose match is gone before it is read now counts as no look, and the act polls to its
deadline as it does for any absence. Where nothing arrives, the refusal says which absence it was:
nothing matched, in 80 looks over 2000 ms, or it matched on 3 of 80 looks and was gone each time
before it could be read. The first points at the locator, the second at the application. A test
asserting the old wording, "nothing matched, or what matched has gone since", sees the new one. If
you put a read step between an expand and a select to get around this, you can delete it.
The plugin's marketplace is named winwright
(May notice.) The Claude Code marketplace was named alegauss, the same as roadkeep's, and
settings key marketplaces by name. A project using both could register only one of them: adding one
replaced the other. It is now named after the plugin:
claude plugin marketplace add alegauss/winwright --scope project
claude plugin install winwright@winwright --scope project
If your .claude/settings.json carries winwright@alegauss, remove that entry and its marketplace,
then run the two commands above.
Installing
<PackageReference Include="Winwright" Version="1.1.4" />
<PackageReference Include="Winwright.InApp" Version="1.1.4" />Full Changelog: v1.1.3...v1.1.4
v1.1.3
This release finishes what 1.1.2 started for a terminal client's cases. The repair to typing into a
terminal is now proven, a full-screen mode can be checked rather than inferred, and a case that runs
first or alone can type. One change adds new format. Two change something an existing suite may
notice, and they are marked.
The terminal repair is proven
1.1.2 taught type to repair the last-character substitution on a terminal. That was unproven until
an adopter ran it: quickshell's tab case, which had failed two runs in about twenty, passed 300 of
300 guest runs in a row on 1.1.2. The repair fired on two of them, one resend each, and each of
those traces says so. No code changed for this; it is why the repair can be relied on.
A step can claim an element covers its monitor
{ "locator": "Document#terminal", "act": "read", "fillsMonitor": true }fillsMonitor claims this step's element covers the whole of the monitor it is mostly on. A case
pressing F11 could read that the title bar had gone, but not that what was left filled the screen, so
a full screen that kept a border passed. Point it at what should fill the screen (the terminal, the
video, the canvas) rather than only at what went away.
A failure names both rectangles and each edge left uncovered, in pixels: 8px at the right, 8px at the bottom is a size taken from the wrong rectangle, and a strip at the bottom alone is usually the
taskbar. Half a pixel of slack, read with no reads, and the case never names a monitor, so it
passes on whichever display the window was put on. It is a claim about rectangles, never about the
colour of a pixel.
A window launched first in its run comes forward
(May notice.) Windows lets a newly started application take the foreground only when the process
that started it could. A test host that had sent no input could not. So a case first in its run, or
one soaked alone, waited out its whole budget behind Program Manager and came back Degraded, while
the same case passed inside a full run.
A launch now sends one mouse move of no distance before it starts anything, then hands the started
process the foreground by its pid. The cursor does not move, and the run counts the move as its own
input, never a person's. The machine's foreground-lock setting is not touched.
LaunchedProcess.Handed says whether the hand-off landed. Where a window still does not come
forward, the hole now ends by saying whether the launch could hand it the foreground. If it could,
the application declined to come forward; if it could not, the desk refused.
If your harness sends input of its own before each launch to get around this, you can delete it.
Smaller
(May notice.) The two lines a run writes about catches now agree with a count of one in every
word. One case used to print "1 case that ran say what they catch"; it now reads "1 case that ran
says what it catches". A test asserting the old wording sees the new one.
Installing
<PackageReference Include="Winwright" Version="1.1.3" />
<PackageReference Include="Winwright.InApp" Version="1.1.3" />Full Changelog: v1.1.2...v1.1.3
v1.1.2
This release came out of one adopter's UI cases: quickshell, a terminal client whose window is a tab
strip and a terminal. A close button that painted nothing passed every case, a sidebar that takes
several rows could only ever be given one, a case was red on any machine whose checkout lived
somewhere else, and typing into the terminal put the wrong character in about one send in fifty.
Five changes add new format. Four change something an existing suite may notice. Those are
marked.
A step can claim where an element is drawn
{ "locator": "Button#closeTab", "act": "read", "within": "TabItem#second" }within names a second locator in the same window and claims this step's element lies inside
that one's rectangle and is not empty. A case could read that an element exists and what it is
called, but not where it is drawn, so a close button 22 by 22 whose glyph the theme's padding left no
room for passed. A failure names both rectangles, and an element laid out to nothing (zero wide,
zero high, or offscreen) fails as empty. Half a pixel of slack absorbs the rounding to device
pixels. The act is read, with no reads beside it.
Adding to a selection instead of replacing it
{ "locator": "List#sessions > ListItem[name=\"Beta\"]", "act": "add to selection", "reads": "selected", "expect": "selected" }select replaces whatever the list had selected. add to selection and remove from selection leave the rest as it was. Both go through the item's own selection pattern, so they
need no foreground. On a list that takes one row at a time, both are refused, naming the list,
because an add there is a replace that reads as an add.
For an application that answers the gesture and offers no pattern, a click takes holding:
ctrl or shift. The key goes down and up in the same batch of input as the click, so nothing can
arrive between them, and the step's name in the report says the key.
A press can tap a modifier alone
"with": "Alt" on its own is a tap: the key goes down and up with nothing between, which is how
Windows shows a classic menu bar. The same goes for Ctrl, Shift and Win. The report calls it
"Alt, alone", so a trace never reads as a chord that lost its key. Two modifiers with no key
(Ctrl+Alt) are still refused.
A step can name the project's root
A step's with now fills {root}, the directory winwright.json sits in, {root/}, the
same path as Windows OpenSSH spells it (/C:/src/app), and {files}, the fixture's staged
directory:
{ "locator": "Edit#remotePath", "act": "set value", "with": "{root/}/cases/fixtures", "reads": "value", "answers": true }A case that typed one desk's checkout path whole was red on every other desk. A brace shaped like a
token that is none of the three is refused, naming them. Any other brace, such as a line of JSON a
case types, is typed as written.
Typing into a terminal
A control that publishes the Text pattern and no Value pattern, such as a terminal whose Text is its
whole screen, is now read as a document. Typing into it has arrived when the screen holds
what was typed. Synthesised Unicode input puts the last character typed where an earlier one belongs
about once a send in fifty. On a terminal, type now takes back exactly what it sent with Backspace
and sends it again, up to three times, which is safe because nothing on the line has run before
Enter. Text carrying Enter or Tab is never repaired, so a case that types a command and runs it
presses Enter as its own step.
(May notice.) A type into a Text-only control is judged by what the screen holds and may
resend. A repaired step stays green, and its trace line now says how many resends it took
(after 1 resend), so the rate can be read off your own runs. A test that expected a type step's
trace detail to be empty sees that sentence where a repair happened.
Staged files are fresh
(May notice.) A file a fixture stages is now written now, as a file the application was just
given, instead of keeping its source's time in the tree. A client that opens a feed only when it was
written in the last ten minutes passed on the day the fixture was made and failed the day after. To
stage a stale one on purpose, an entry may say "modified": "-1d". An age is now, or a minus and a
whole number of s, m, h or d. Anything else is refused where the fixture is declared.
A run names claims that only repeat their case's name
(May notice.) Beside the cases that say nothing about what they catch, a run's render now names
the cases whose catches begins or ends with the case's own name, quoting the claim. It is a line for
a person to read: it never refuses a case and never moves the exit code. Names under eight
characters are left out, since a claim that starts with a word or two of its name is ordinary
prose.
Installing
<PackageReference Include="Winwright" Version="1.1.2" />
<PackageReference Include="Winwright.InApp" Version="1.1.2" />Full Changelog: v1.1.1...v1.1.2
v1.1.1
This release finishes the drop onto Explorer that 1.1.0 started, gives a click its other buttons, and
closes a gap a reader of the 374-to-352 article measured in the claim diff. Three changes add new
format. One refuses a case file that loaded before, and it is marked may notice.
A step can read a file on disk
1.1.0 let a drag drop a file onto an Explorer folder, and left checking that it arrived to "the next
step", which no step could do: a file on disk is in no window's tree. A step may now name a file
instead of a locator or a tray icon. It takes read and makes one claim, sameBytesAs:
{ "file": "{files}/landing/report.bin", "act": "read", "sameBytesAs": "cases/fixtures/report.bin" }Both paths are relative to the project's root or name the fixture's staged directory with {files}.
The step waits over timeouts.resolve, because Explorer writes a dropped file after the release
returns. A file still missing then is a failure naming what the folder holds. One that differs names
both lengths and the first byte that differs. A file step without sameBytesAs is refused, because
a file that merely exists is what a truncated copy is too.
A fixture can open an Explorer folder beside the window
{ "name": "with a landing folder", "files": ["cases/fixtures/report.bin"], "opens": "{files}/landing" }opens creates the folder and opens it in Explorer before the case starts. The window under test
and the folder go side by side on the primary display, so a drop released over the folder lands in
it, and the desk is handed back to the window under test. The folder's window is closed when the case
gives its window back. One still standing after timeouts.stop is named among the survivors, and
one that never opens makes the case a hole naming the desk. opens beside shareable is refused.
With the two together, a drag onto Explorer and the check that the file landed byte for byte is a
case file, with no C# beside it.
A click can press the middle or the right button
{ "locator": "TabItem#second", "act": "click", "with": "PointerIsTheAct", "button": "middle" }A middle click closes a tab in every browser and terminal on Windows, and a right click opens a
context menu. click used to press only the left button. button takes left, middle or
right, and defaults to left, so no existing case changes. The step's name in the report says the
button. A name that is not a button is refused with the three there are, and button beside any act
but click is refused.
A catches that only repeats the case's name is refused
(May notice.) 1.1.0's lastRun compares what each case catches from one run to the next, so a
rename that keeps its claim stays quiet. A reader of the 374-to-352 article measured what happens when
the claim is just the case name again: the comparison turns back into a comparison of names, reports
every rename as a lost claim, and misses a case that kept its name and changed what it checks.
A case whose catches equals its own name, ignoring case, spacing and punctuation, is now refused
where it is written. A claim that only contains the name inside a longer sentence is accepted. If a
suite of yours stops loading on this, write what the case catches rather than what it is called.
Full Changelog: v1.1.0...v1.1.1
v1.1.0
This release came out of a file browser's drag-and-drop, an intermittent leftover process, and a
reader's question about a suite that keeps its total while its contents change. Two of the changes
add new format, and three change a sentence an existing suite may notice. Those are marked.
Drop onto another application's window
{ "locator": "ListItem[name=\"report.txt\"]", "act": "drag", "to": "Window[name=\"Downloads\"] > List", "toDesktop": true }A drag used to look for its to only in the window it started in, so a file dragged onto an
Explorer folder had no case. With "toDesktop": true, the to locator is looked up from the
desktop instead, so its first step names the other window. Only the release lands there: the
foreground the drag needs is still the window under test, and whether the file arrived is the next
step's to check (for a drop onto Explorer, that is a file on disk, not an element). toDesktop
without a to is refused at load.
A run says when its cases changed since the last one
A count catches 374 becoming 352. It cannot catch one test turned into a skip and another renamed,
which keeps the total and exits 0 with the line people read unchanged. Declare lastRun in
winwright.json:
{ "lastRun": "TestResults/last-run.json" }and every run keeps the names of the cases it ran, per selection, each with its catches. The next
run over the same selection compares against it, on the line under the verdict's sentence:
the cases that ran are not the ones the last run over everything ran: 1 appeared ('renamed'), and
1 vanished ('renames'); every claim the last run made is still made.
What it judges is the claim, not the name. A rename that kept its catches holds. A claim no case
that ran makes any more is named as lost. A case that vanished without a catches is named as
one whose loss cannot be told. Two cases making the same claim cover each other. It is a finding and
never moves the exit code. The file is rewritten on every run, so a deliberate change is named once,
on the run that made it.
(May notice.) SuiteVerdict.Render() now always has this line second, right after the
sentence. Without lastRun it says nothing remembers the last run. Anything that reads the rendered
lines by position moves by one.
A leftover process names the case that launched it (may notice)
A process that outlived its case used to be named by pid alone, which in a sixteen-case run is
sixteen guesses, and the pid was usually gone by the time anybody read it. The register is now told
which case each launch is for:
pid 4812 YourApp.exe, launched for 'connected tab reads its bars' - outlived its case and would not stop
A launch lent to several read-only cases says it was lent to the cases after the first. The
early-exit line (EarlyExits) names the case the same way. ProcessRegister.Launch takes the case
name as an optional argument, and Survivor and Departure carry it.
A lost foreground says whose window has it (may notice)
When an act could not get the foreground because another process's window held it, the hole now
says whether that process is one the run launched, and for which case, or one no case in this run
started:
the foreground belongs to quickshell (pid 7500) 'quickshell', which no case in this run started,
and the window under test is YourApp (pid 5536) 'Main'
The first points at a window an earlier case left behind. The second, on a shared machine, is
somebody else's run or the operator.
One run per guest desk
tools\run-tests-vm.ps1 now claims the guest desk for its whole run, through a host-wide mutex
named winwright-guest-desk-<hash of the .vmx path>. Two runs on one guest used to take each
other's foreground and read it, honestly, as a desk they could not get. A second run now waits,
naming the run that holds the desk, and refuses if the wait outlasts the holder's bound (the run
bound plus 15 minutes). An adopter with a runner of its own can take the same mutex.
Full Changelog: v1.0.2...v1.1.0
v1.0.2
Most of this release came from adopting cases in a terminal client: its tab strip, its connection
states and its session store. Everything below is new format, and three changes are ones an
existing suite may notice. They are marked.
Drag one element onto another
{ "locator": "TabItem#firstTab", "act": "drag", "to": "TabItem#secondTab" },
{ "locator": "Text#tabOrder", "act": "read", "reads": "name", "expect": "Second, First, Third" }drag presses on the step's element, moves past the system drag threshold in paced steps, and
releases over the element to names in the same window. It is for the gesture no UI Automation
pattern expresses, such as a tab strip reordered by dragging. Like click, it needs the foreground,
and a desk that won't give it is a hole rather than a red. A drag with no to is refused at load,
and so is a to beside any other act.
Read the state an element publishes, and find an element by it
"reads": "status" returns UI Automation's ItemStatus: the words a screen reader announces for a
coloured dot, such as Connected or Reconnecting. An element that publishes none reads as the
empty string, not as nothing observed. A locator can select by it, so a case finds the connected tab
without knowing its title:
{ "locator": "TabItem[status=\"Connected\"]", "act": "read", "reads": "name", "answers": true }Claim that a reading is empty
"empty": true claims the element is there and its reading says nothing, such as a field a form
must leave blank or a tab with no state to report. Unlike absent, a locator that matches nothing
fails it. It is refused beside a reading such as focused that answers for every element.
A reading the element cannot give fails at once (may notice)
A step reading value off a ComboBox that is not editable used to poll its whole deadline and fail
with "nothing answered". That ComboBox offers no Value pattern, so the step now fails immediately,
naming the missing pattern and the readings the element does give:
ComboBox #SignIn has no Value pattern, so 'value' can never answer for it, and what it does offer
reads as 'picked', 'expanded', 'name'
The verdict is the same red as before. What changes is that it arrives seconds sooner, and its
sentence tells you which reading to use instead.
A case that stops short closes what it expanded
When a case stops partway (a step threw, or an act was never sent), it now collapses whatever its
expand steps opened and its own collapse steps had not closed. This goes through the pattern, so
the tidy needs no foreground, and each collapse is a line in the trace. Before this, a drop-down left
open by one red case could hold the desk and degrade the next case. A case that runs to its end is
left exactly as its last step left it.
Fixtures: servers, staged layouts, and filled stores
reaches names what the application connects to outside itself, as a host:port or a
%VARIABLE%. Before every launch the run opens a TCP connection to each one, using
timeouts.reach (2000 ms by default). If any is not listening, the application is never
launched, and every check in the case is a hole naming the address. If all are, {reach0},
{reach1} and so on in arguments and variables become the addresses that were reached.
winwright starts no server; that stays the project's job.
A files entry can say where it lands under the staged directory, for a store one folder down:
"files": [
{ "from": "cases/fixtures/sessions.json", "to": "YourApp/sessions.json", "fill": true },
"cases/fixtures/theme.json"
]A to that is absolute, or climbs out with .., is refused. (May notice.) Two entries landing
on the same path are now refused at load, naming both. That includes two plain files with the same
name from different folders, which used to overwrite each other silently. A suite that did this was
reading whichever file was declared last.
"fill": true replaces {reach0} and so on, and {files}, inside the staged copy's text, so a
store names its server once, in reaches. A file landing as .json has every replacement
JSON-escaped, so "store": "{files}" parses. Any other file is filled raw. A file asked to be filled
that is not UTF-8 is refused by name.
One declaration for every build
A path in winwright.json may carry {configuration}:
{ "executable": "bin/{configuration}/net10.0-windows/YourApp.exe", "configuration": "Debug" }The value comes from, in order: the configuration passed to ProjectDeclaration.Load(path, configuration), then the WINWRIGHT_CONFIGURATION environment variable, then the file's own
configuration. CI sets the variable for its Release job, and a developer's runner gets the default
without doing anything. A path whose token nothing fills is refused when it is asked for, naming the
key and the variable, so a run never launches a path with a brace in it.
A redirected profile says it only reaches some applications (may notice)
A fixture that sets APPDATA, LOCALAPPDATA, USERPROFILE or HOME now says so on its own line
and on every case line run against it. The sentence is setting APPDATA as a variable only — an
application asking Windows for that known folder still reads the real profile. Most Windows
applications find their data through the known folder, which ignores the variable. A case that
staged a store that way could be green about the real profile's store. Nothing is refused, because
winwright cannot tell the two kinds of application apart, but the report no longer reads as though
the redirect covered both.
What's Changed
- chore(deps): bump sharp from 0.35.4 to 0.35.5 in /site/docs by @dependabot[bot] in #4
- chore(deps): bump smol-toml from 1.8.0 to 1.9.0 in /site/docs by @dependabot[bot] in #3
- chore(deps): bump source-map-js from 1.2.1 to 1.2.2 in /site/docs by @dependabot[bot] in #2
Full Changelog: v1.0.1...v1.0.2
v1.0.1
Two things, and the first is the one to look at: this release carries a documentation area,
published at alegauss.github.io/winwright/docs and
written for a reader who has installed nothing.
Every figure on it is read out of the engine's own C# at build time, never typed: the 121 verbs with
what each needs of the application, the case format straight from the loader's schema, the locator
grammar out of the parser, every winwright.json key with what leaving it out does, and all four
verdicts with their exit codes. A retyped table is wrong at the first rename and silent about it, so
the build fails rather than publishing a figure that drifted. The reference lines an adopter copies
ask nuget.org for the version as the page loads, because the site deploys on demand and cutting a
release does not trigger it — so a generated number would be a version behind from the next release
onward, on exactly the line somebody pastes into a csproj.
The second is the launch, and it carries changes an adopter may notice.
A launch now has a working directory
Found adopting a case for a terminal client. A fixture passing ["--import", "cases/fixtures/profile.ini"] was passing a path the launched application resolved against
whichever directory the test runner happened to be in, so the same argument named three different
files under run-tests.cmd, under dotnet test and in CI.
A launch now starts in the project's root — the directory winwright.json sits in, which is
already what every path that file declares resolves against. A fixture may name another with the new
workingDirectory key, resolved against that same root.
A fixture can give the application a store of its own
The other half of the same problem: a case that needs a known data file had two choices, both bad —
a command-line flag that exists only for the harness, which the application's users then see, or
writing into the real user profile before launching, which the next case tramples.
{
"fixtures": [
{
"name": "with sessions",
"variables": { "APPDATA": "{files}" },
"files": ["cases/fixtures/sessions.json"]
}
]
}files are copied into a directory made for that launch and emptied first, and {files} names
that directory from a variable or from workingDirectory. So a Windows application needs no flag
for the harness at all. The directory is under the system temp rather than in your checkout, because
an engine that writes into your working tree is one that turns up in your git status.
The trace says more about the desk
A step whose keystroke was delivered and one whose desk check passed because an ancestor merely
shared a top-level window used to leave the same record: ok. Trace lines now carry a met field
saying who held the foreground and which of the two findings made it ours — the mirror of the
detail a step that did not run has always carried. Nothing about a verdict changed.
Also: every reading a step may name now ships with the sentence an author needs to pick one, so
picked and selected can be told apart without reading the source.
If you are upgrading
Source-compatible for a case file — workingDirectory and files are both optional, and a fixture
that declares neither behaves as before, except that the launch now starts in the project root
instead of wherever the runner stood. If a fixture argument carries a relative path, check what it
used to resolve against.
Two public API signatures moved, for callers driving the engine directly rather than through a case
file:
FixtureDeclaration.Starting(executable)now takes the project root as well. A default there
would have been the defect this release removes, spelled as a choice nobody made.FixtureDeclaration.Stage(root)hands back aStagedFixture, and composing a launch from it is
what guarantees the files are in place before the process reads them.Startingrefuses where a
fixture needs the staged directory and nothing has staged it.
<PackageReference Include="Winwright" Version="1.0.1" />
<PackageReference Include="Winwright.InApp" Version="1.0.1" />Full Changelog: v1.0.0...v1.0.1
v1.0.0
The first stable release, after eighteen alphas.
winwright drives a Windows desktop application from a test and reports what was actually
observed. It exists because of one measurement: a suite reported a pass with no failures and a
total of 352, where the run before it had 374. Twenty-two tests were gone, the test host had died
partway through, and the only sign was a number nobody had a reason to read. Everything here
follows from refusing that — a green may not cover a check that never ran, and anything the tool
could not evaluate is named in the output rather than left out of it.
That is why a run has four outcomes and not two: Passed (0), Failed (1), Degraded (2) and
Broken (3). Degraded is the one that matters — everything that ran passed, and something could not
be evaluated at all. CI reads the exit code, so the distinction survives the pipeline.
What 1.0 means
The engine answers for itself. 121 verbs, a locator grammar with 13 forms and 13 refusals,
and a case format of 2 file keys, 9 case keys, 31 step keys and 10 fixture keys — all of it read
out of the engine's own source, so the published figures cannot drift from the code.
What settles the version is a deletion rather than a feature. The adopting application's own
3,004-line PowerShell interaction harness is gone: 23 cases now check 95 claims in 128–159 s, where
the script checked 31 in 46 s and crashed. That was the last open line of the block whose name is
the proof is the deletion.
Also in this release: the display precondition answers absent on a console session that
disconnected itself, where counting monitors and measuring the virtual screen both still said
present — and met once reconnected. A desk that renders nothing is a hole, not a pass.
Installing
<!-- In the test project that drives the application. -->
<PackageReference Include="Winwright" Version="1.0.0" />
<!-- In the application under test, only if you want the readings it can only take
from inside the process. -->
<PackageReference Include="Winwright.InApp" Version="1.0.0" />Windows and .NET 10. The engine is UI Automation throughout and is not cross-platform. Neither
package carries an external dependency.
Reading further
- alegauss.github.io/winwright — the short version of the
argument. - README — the long form: every verb
family, every field a case may carry, and the reason behind each refusal. - CHANGELOG — every line
shipped, with what it cost and what it proved.
Full Changelog: v0.1.0-alpha.18...v1.0.0