@orkestrel/scaffold 0.0.73 → 0.0.74

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -44,7 +44,8 @@ production code. Source: [`src/core`](../src/core), [`src/browser`](../src/brows
44
44
  [`src/server`](../src/server).
45
45
 
46
46
  This package runtime-depends on `@orkestrel/contract` for the outcome type `retryUntil` reads
47
- internally. That type is not re-exported. No exported type here names an `@orkestrel/*` type. A
47
+ internally and for the guards every environment narrows with. Nothing from it is re-exported, and no
48
+ exported type here names a type from another `@orkestrel` package. A
48
49
  dependency on `@orkestrel/emitter` would install a second copy of it beside the one a consumer
49
50
  already pins, and the compiler reads two copies as two distinct types. A foreign type in a
50
51
  signature fails the other way, rejecting the consumer's own local value inside the consumer's own
@@ -111,23 +112,26 @@ member and `plus` introducing its call-signature members, and a type alias's own
111
112
  a union's arms escaped as `\|`. An extended interface's name comes before `plus`, with the members
112
113
  it adds after.
113
114
 
114
- | Type | Kind | Shape | Summary |
115
- | -------------------------- | --------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
116
- | `WaitOptions` | interface | `{ budget?, interval?, signal? }` | Configures a bounded asynchronous wait with an elapsed-time limit, a delay between readings, and an abort signal. |
117
- | `RetryOptions` | interface | `WaitOptions` plus `{ attempts? }` | Configures a bounded retry, adding an optional producer-call limit to a bounded wait's bounds. |
118
- | `EventSubscriber` | type | `(listener) => cleanup \| void` | Subscribes a listener to one event source. |
119
- | `RecorderInterface` | interface | `{ calls, count, handler }` plus `clear` | Records every call made to its handler. |
120
- | `EventSourceInterface` | interface | `{} plus on` | Subscribes handlers to a typed event source. |
121
- | `RecorderMap` | type | `{ readonly [K in TName]: RecorderInterface<TMap[K]> }` | Maps event names to recorders for their delivered argument tuples. |
122
- | `SignalInterface` | interface | `{ controller, signal, count }` | Holds a real abort signal and controller instrumented with its live abort-listener tally. |
123
- | `SignalRegistration` | type | `readonly [listener, installed, capture, cleanup]` | Represents one abort listener an instrumented signal installed, as its tally holds it. |
124
- | `ResourceFactoryInterface` | interface | `{ created, destroyed }` plus `create` / `destroy` | Represents a numbered resource factory with records of every creation and destruction. |
125
- | `TeardownInterface` | interface | `{ count }` plus `add` / `destroy` | Represents the cleanup a test adds as it goes and runs once, newest first, when it is done. |
126
- | `TeardownHandler` | type | `() => void \| Promise<void>` | Represents the work one teardown entry performs when the list is destroyed. |
127
- | `JSONSafe` | type | `JSONSafe<T>` | Represents the JSON-safe projection of a type: every member JSON preserves, mapped to itself, and every member it does not, mapped to `never`. |
128
- | `HeadersSource` | type | `NonNullable<ConstructorParameters<typeof Headers>[0]>` | Covers any value the host `Headers` constructor accepts. |
129
- | `StateTransition` | interface | `{ name, from, event, to }` | Represents one row of a statechart table: the entity's state before an event, the event, and the state that event must leave it in. |
130
- | `StateScenario` | interface | `{ transition }` plus `arrange` / `act` / `assert` | Drives one `StateTransition` through the three phases that prove it. |
115
+ | Type | Kind | Shape | Summary |
116
+ | -------------------------- | --------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
117
+ | `WaitOptions` | interface | `{ budget?, interval?, signal? }` | Configures a bounded asynchronous wait with an elapsed-time limit, a delay between readings, and an abort signal. |
118
+ | `RetryOptions` | interface | `WaitOptions` plus `{ attempts? }` | Configures a bounded retry, adding an optional producer-call limit to a bounded wait's bounds. |
119
+ | `TextWaitOptions` | interface | `WaitOptions` plus `{ exact?, absent? }` | Configures a bounded wait over a reading of text. |
120
+ | `EventSubscriber` | type | `(listener) => cleanup \| void` | Subscribes a listener to one event source. |
121
+ | `RecorderInterface` | interface | `{ calls, count, handler }` plus `clear` | Records every call made to its handler. |
122
+ | `EventSourceInterface` | interface | `{} plus on` | Subscribes handlers to a typed event source. |
123
+ | `RecorderMap` | type | `{ readonly [K in TName]: RecorderInterface<TMap[K]> }` | Maps event names to recorders for their delivered argument tuples. |
124
+ | `SignalInterface` | interface | `{ controller, signal, count }` | Holds a real abort signal and controller instrumented with its live abort-listener tally. |
125
+ | `SignalRegistration` | type | `readonly [listener, installed, capture, cleanup]` | Represents one abort listener an instrumented signal installed, as its tally holds it. |
126
+ | `ResourceFactoryInterface` | interface | `{ created, destroyed }` plus `create` / `destroy` | Represents a numbered resource factory with records of every creation and destruction. |
127
+ | `TeardownInterface` | interface | `{ count }` plus `add` / `destroy` | Represents the cleanup a test adds as it goes and runs once, newest first, when it is done. |
128
+ | `TeardownHandler` | type | `() => void \| Promise<void>` | Represents the work one teardown entry performs when the list is destroyed. |
129
+ | `JSONSafe` | type | `JSONSafe<T>` | Represents the JSON-safe projection of a type: every member JSON preserves, mapped to itself, and every member it does not, mapped to `never`. |
130
+ | `HeadersSource` | type | `NonNullable<ConstructorParameters<typeof Headers>[0]>` | Covers any value the host `Headers` constructor accepts. |
131
+ | `JourneyVariant` | interface | `{ name, width, height }` | Represents one theme-and-viewport pair in the form a project configuration can serialize. |
132
+ | `StatechartStatus` | type | `'pending' \| 'idle' \| 'running' \| 'passed' \| 'failed'` | Names the run state a statechart harness publishes through its status attribute. |
133
+ | `StateTransition` | interface | `{ name, from, event, to }` | Represents one row of a statechart table: the entity's state before an event, the event, and the state that event must leave it in. |
134
+ | `StateScenario` | interface | `{ transition }` plus `arrange` / `act` / `assert` | Drives one `StateTransition` through the three phases that prove it. |
131
135
 
132
136
  Each interface's call-signature members are listed under [Methods](#methods). `Result`, `Success`,
133
137
  and `Failure` come from `@orkestrel/contract` (mirrored at [`contract.md`](contract.md)) and are not
@@ -145,8 +149,13 @@ A `Shape` cell holds the constant's declared type.
145
149
  A harness renders the attributes and a gate outside the page polls them, so the names are the whole
146
150
  contract between the two. `status`, `passed`, `failed`, and `total` belong on the harness root,
147
151
  `scenario` and `result` on each row, and `state` on the element rendering the entity's current
148
- state. `pending` is what a harness carries before a run has produced a result for every row, and
149
- `passed` and `failed` are the pair a gate waits for rather than waiting a fixed duration.
152
+ state. `pending` is what a harness carries while its inventory is incomplete — until every
153
+ declared row has rendered and the root carries the row count — so a gate that reads it has found
154
+ a harness whose rows never mounted. `idle` is a mounted harness with its tally at zero, `running`
155
+ is a run in flight, and `passed` and `failed` are the pair a gate waits for rather than waiting a
156
+ fixed duration. An exceptional exit is terminal too: a harness whose `state` reader throws writes
157
+ `failed` and then rejects the run, so the gate reads a terminal pair while the suite reads the
158
+ throw. `StatechartStatus` is the same set of readings as a named union.
150
159
 
151
160
  #### Validators
152
161
 
@@ -173,8 +182,10 @@ instead of propagating.
173
182
  | `waitForCondition` | function | `(description, condition, options?) => Promise<void>` | Waits until a condition holds within an elapsed-time budget. |
174
183
  | `retryUntil` | function | `(description, produce, satisfied, options?) => Promise<T>` | Repeats a producer until one produced value satisfies a predicate. |
175
184
  | `waitForEvent` | function | `(subscribe, description, options?) => Promise<TArgs>` | Waits for the first delivery from an event subscription. |
185
+ | `waitForText` | function | `(description, read, text, options?) => Promise<string>` | Waits until a reading of text carries an expected sentence. |
176
186
  | `checkBounds` | function | `(subject: string, budget: number, interval: number) => void` | Checks the resolved bounds one bounded wait runs under. |
177
187
  | `buildRetryExhausted` | function | `(description, budget, elapsed, last, cause) => Error` | Builds the error `retryUntil` raises when its elapsed-time budget runs out. |
188
+ | `buildRefusal` | function | `(name: string, cause: unknown) => Error` | Builds the error a refused fixture build raises, named for the row it was building for. |
178
189
  | `dropRegistration` | function | `(registrations: SignalRegistration[], installed) => SignalRegistration \| undefined` | Drops the registration an instrumented signal installed for one listener. |
179
190
  | `decodeJSONLines` | function | `(text: string) => readonly unknown[]` | Decodes newline-delimited JSON values. |
180
191
  | `waitForDelay` | function | `(ms?: number) => Promise<void>` | Waits for a host timer to elapse. |
@@ -216,12 +227,16 @@ a journey a description of what a person does rather than of what the markup hap
216
227
 
217
228
  The fixture builders, the readers, and the field writers do take an element, and none of them is a
218
229
  journey verb. `build` creates a node, `mount` attaches one, and `render` does both from
219
- markup or from a tag and its classes; `clearStorage` takes nothing at all, and `removeDatabase`
220
- takes a database name. The predicates, the element readers, and the describers name a node the
221
- caller already has — `isRendered`, `isReachable`, `readHit`, `readText`, `readRole`, `readName`,
222
- `readStates`, `describeTree`, `describeFocus`, `extractOrphans`, `readRows`, `readStyle`,
223
- `readToken`, `readPixels`, `readContrast`, `readLayers`, `readBackdrop`, and `readRing` — and each
224
- reads that node rather than acting on a target it was handed. `captureFrame` and `place` take an
230
+ markup or from a tag and its classes; `createHarness` mounts a whole statechart table and hands
231
+ back the root it mounted; `buildContrast`, `buildEscapes`, and `buildCensus` each build
232
+ a detached control the caller appends where it is reading; `clearStorage` takes nothing at all, and
233
+ `removeDatabase` takes a database name. The predicates, the element readers, and the describers name
234
+ a node the caller already has — `isRendered`, `isReachable`, `readHit`, `readText`, `readRole`,
235
+ `readName`, `readStates`, `readCensus`, `describeTree`, `describeFocus`, `extractOrphans`,
236
+ `readRows`, `readStyle`, `readToken`, `readPixels`, `readContrast`, `readLayers`, `readBackdrop`,
237
+ and `readRing` — and each reads that node rather than acting on a target it was handed.
238
+ `waitForAnimations` takes one too, and waiting for a browser to stop painting it changes nothing
239
+ about it. `captureFrame` and `place` take an
225
240
  element as well, and photographing one is a reading too: neither moves focus, dispatches an event,
226
241
  or changes what the element renders. `typeInput` and `commitInput` are the exception, and it stays
227
242
  narrow: they write into the field they are given, as the synthetic counterpart of `typeAccessible`
@@ -234,17 +249,26 @@ A `Shape` cell holds an interface's data members as bare names in braces, `?` ma
234
249
  member and `plus` introducing its call-signature members, and a type alias's own type literal with
235
250
  a union's arms escaped as `\|`.
236
251
 
237
- | Type | Kind | Shape | Summary |
238
- | -------------------- | --------- | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
239
- | `Color` | type | `readonly [red, green, blue, alpha]` | Represents one rendered color as straight sRGB channels and its alpha. |
240
- | `ElementOptions` | interface | `{ classes?, text?, attributes? }` | Configures one built element: its class list, its text, and its attributes. |
241
- | `FrameOptions` | interface | `{ path, width, height, element? }` | Configures one captured frame: where it is written, the viewport it is shot at, and what it shoots. |
242
- | `FrameReading` | interface | `{ width, height, floor }` | Represents one written frame read back from the file a capture produced: its size in device pixels, and the single color its bottom row paints. |
243
- | `CaptureVariant` | interface | `{ name, width, height, apply? }` | Represents one theme-and-viewport pair a capture run renders, and the document change it needs first. |
244
- | `PortfolioOptions` | interface | `{ states, variants, variant, directory, enabled? }` | Configures a capture portfolio: the state registry, the variant matrix, this run's variant, where it writes, and whether it writes at all. |
245
- | `PortfolioInterface` | interface | `{ variant, placements, paths, files }` plus `place` | Holds the registry of capture states one run places, and the files it wrote placing them. |
246
- | `JournalStep` | interface | `{ action, trigger, result }` | Represents one scripted step a journal recorded, and what the surface did about it. |
247
- | `JournalInterface` | interface | `{ steps, output }` plus `start` / `stop` / `record` | Records one scenario: every step it took and everything the page said while it ran. |
252
+ | Type | Kind | Shape | Summary |
253
+ | --------------------- | --------- | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
254
+ | `Color` | type | `readonly [red, green, blue, alpha]` | Represents one rendered color as straight sRGB channels and its alpha. |
255
+ | `ElementOptions` | interface | `{ classes?, text?, attributes? }` | Configures one built element: its class list, its text, and its attributes. |
256
+ | `FrameOptions` | interface | `{ path, width, height, element? }` | Configures one captured frame: where it is written, the viewport it is shot at, and what it shoots. |
257
+ | `FrameReading` | interface | `{ width, height, floor }` | Represents one written frame read back from the file a capture produced: its size in device pixels, and the single color its bottom row paints. |
258
+ | `CaptureVariant` | interface | `JourneyVariant` plus `{ apply? }` | Adds to a journey variant the document change a capture run applies before resizing. |
259
+ | `PortfolioOptions` | interface | `{ states, variants, variant, directory, enabled? }` | Configures a capture portfolio: the state registry, the variant matrix, this run's variant, where it writes, and whether it writes at all. |
260
+ | `PortfolioInterface` | interface | `{ variant, placements, paths, files }` plus `place` | Holds the registry of capture states one run places, and the files it wrote placing them. |
261
+ | `JournalStep` | interface | `{ action, trigger, result }` | Represents one scripted step a journal recorded, and what the surface did about it. |
262
+ | `JournalInterface` | interface | `{ steps, output }` plus `start` / `stop` / `record` | Records one scenario: every step it took and everything the page said while it ran. |
263
+ | `StateOptions` | interface | `WaitOptions` plus `{ absent? }` | Configures a bounded wait over the states a control announces. |
264
+ | `StorageOptions` | interface | `{ values?, reads?, writes?, quota? }` | Configures an inert `Storage`: its seed, which operations the host permits, and its quota. |
265
+ | `WebStorageInterface` | interface | `Storage` plus `{}` plus `permit` | Holds a store the host can withhold and later grant. |
266
+ | `CensusReading` | interface | `{ elements, tokens, undeclared }` | Reports an authored-class census: the population walked, the tokens found, and the undeclared. |
267
+ | `ContrastFixture` | interface | `{ root, refused, accepted }` | Holds a detached translucent stack whose flat and composited readings disagree across one bar. |
268
+ | `EscapeFixture` | interface | `{ root, inline, embedded, permitted }` | Holds detached markup a style-escape reading must find, and the one it must leave alone. |
269
+ | `CensusFixture` | interface | `{ root, token, mark }` | Holds detached markup an authored-class census must report as undeclared. |
270
+ | `HarnessOptions` | interface | `{ scenarios, build, state, pause? }` | Configures the harness that renders one transition table and drives it row by row. |
271
+ | `HarnessInterface` | interface | `{ root, status, total, passed, failed, failures }` plus `execute` / `destroy` | Holds a mounted statechart harness, the tally it publishes, and the run it drives. |
248
272
 
249
273
  #### Constants
250
274
 
@@ -264,67 +288,76 @@ A `Shape` cell holds the constant's declared type.
264
288
 
265
289
  #### Helpers
266
290
 
267
- | API | Kind | Signature | Summary |
268
- | ----------------------- | -------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
269
- | `resolveAccessible` | function | `(name: string) => HTMLElement` / `(role: string, name: string) => HTMLElement` | Resolves one visible, focus-reachable interactive element by its exact accessible name. A wholly-off-viewport target is scrolled into view before reachability is measured. |
270
- | `resolveRendered` | function | `(first: string, second?: string) => HTMLElement` | Resolves one rendered, focus-reachable interactive element without requiring it to intersect the viewport yet. |
271
- | `computeNamePattern` | function | `(name: string) => RegExp` | Computes the pattern that matches one accessible name a decorative glyph may sit beside. |
272
- | `isOutsideViewport` | function | `(rectangle: DOMRectReadOnly) => boolean` | Determines whether a rectangle lies wholly outside the browser viewport. |
273
- | `isRendered` | function | `(element: Element) => boolean` | Determines whether the accessibility tree presents one element at all. |
274
- | `isReachable` | function | `(element: Element) => boolean` | Determines whether a person can click one element where it sits. |
275
- | `readHit` | function | `(element: Element) => Element \| undefined` | Reads the topmost element at one element's bounding-box centre. |
276
- | `clickAccessible` | function | `(name: string) => Promise<void>` / `(role: string, name: string) => Promise<void>` | Clicks one visible, focus-reachable control by its accessible name through the browser provider. |
277
- | `clickAccessibleWithin` | function | `(region: string, role: string, name: string) => Promise<void>` | Clicks one human-reachable control by role and accessible-name text inside a named region. |
278
- | `clickDisclosure` | function | `(name: string) => Promise<void>` | Opens or closes one native details disclosure by its rendered summary. |
279
- | `typeAccessible` | function | `(name: string, text: string) => Promise<void>` | Replaces a named field's value through focus, select-all, deletion, and real keystrokes. |
280
- | `fillAccessible` | function | `(name: string, text: string) => Promise<void>` | Replaces a named field's value in one operation, for text too long to type key by key. |
281
- | `traverseAccessible` | function | `(name: string) => Promise<HTMLElement>` | Reaches a named control only through natural forward Tab traversal from the current focus. |
282
- | `readPerception` | function | `(name: string) => string` | Reads the normalized visible text of one named region, dialog, table, tab panel, or alert. |
283
- | `readPage` | function | `() => string` | Reads the normalized visible text of the whole page. |
284
- | `readFocus` | function | `() => string \| undefined` | Reads the rendered text of the element that holds focus. |
285
- | `readValue` | function | `(role: string, name: string) => string` | Reads the value a resolved control renders. |
286
- | `readText` | function | `(element: Element) => string` | Reads one element's rendered text the way a name computation reads it. |
287
- | `readRole` | function | `(element: Element) => string \| undefined` | Reads the role one element carries in the accessibility tree. |
288
- | `readName` | function | `(element: Element) => string` | Reads the accessible name one element is announced under. |
289
- | `readStates` | function | `(element: Element) => readonly string[]` | Reads the states one element is announced in. |
290
- | `describeTree` | function | `(element: Element) => string` | Describes the accessible tree one rendered element presents. |
291
- | `describeFocus` | function | `(element: Element) => string` | Describes the order sequential keyboard navigation visits one element's controls in. |
292
- | `waitForFrame` | function | `() => Promise<void>` | Waits for one animation frame to settle pending browser paint work. |
293
- | `build` | function | `<K extends keyof HTMLElementTagNameMap>(tag: K, options?: ElementOptions) => HTMLElementTagNameMap[K]` | Builds one unmounted element of a known tag, wearing the classes, text, and attributes asked for. |
294
- | `mount` | function | `<T extends Element>(element: T) => T` | Puts one element into the document and hands it straight back. |
295
- | `render` | function | `(markup: string) => HTMLDivElement` / `(tag: K, classes: string) => HTMLElementTagNameMap[K]` | Renders one fixture into the document from trusted markup. |
296
- | `typeInput` | function | `(element: HTMLInputElement \| HTMLTextAreaElement, text: string) => void` | Sets one field's value and announces it the way typing into the field does. |
297
- | `commitInput` | function | `(element: HTMLInputElement \| HTMLTextAreaElement, text: string) => void` | Sets one field's value and commits it, the way typing and then leaving the field does. |
298
- | `clearStorage` | function | `() => void` | Clears both browser storage surfaces. |
299
- | `removeDatabase` | function | `(name: string) => Promise<void>` | Deletes one IndexedDB database and reports what the request actually did. |
300
- | `parseColor` | function | `(value: string) => Color \| undefined` | Parses one computed CSS color value into straight sRGB channels. |
301
- | `parseCSSColor` | function | `(value: string) => Color \| undefined` | Resolves any CSS color expression to straight sRGB channels, by asking the browser. |
302
- | `matchesColor` | function | `(first: string \| Color, second: string \| Color) => boolean` | Determines whether two colors render the same, within the rounding a browser does. |
303
- | `blendColor` | function | `(front: Color, back: Color) => Color` | Composites one color over another. |
304
- | `measureLuminance` | function | `(color: Color) => number` | Measures one opaque color's WCAG relative luminance. |
305
- | `measureContrast` | function | `(front: Color, back: Color) => number` | Measures the WCAG 2.x contrast ratio between two opaque colors. |
306
- | `readLayers` | function | `(element: Element) => readonly Color[]` | Collects the painted layers standing between one element and the surface it sits on. |
307
- | `readBackdrop` | function | `(element: Element, floor: Color) => Color` | Resolves the opaque color standing behind one element. |
308
- | `readContrast` | function | `(element: Element, floor?: Color) => number` | Measures the WCAG 2.x contrast ratio between an element's computed text and background colors. |
309
- | `readRing` | function | `(control: Element, worn?: Element) => number \| undefined` | Measures the contrast the focus chrome painted on one control reaches against its own backdrop. |
310
- | `measureContent` | function | `() => number` | Measures the row the document's own content ends on, in document coordinates. |
311
- | `stagePane` | function | `(width: number, height: number) => Promise<void>` | Sets the tester's viewport and renders the runner's pane at the size that viewport claims. |
312
- | `releasePane` | function | `() => Promise<void>` | Hands the tester pane back to the runner's own layout, at the viewport it had before staging. |
313
- | `captureFrame` | function | `(options: FrameOptions) => Promise<string>` | Shoots one frame at one viewport size and proves the file on disk holds this run's bytes. |
314
- | `readFrame` | function | `(path: string) => Promise<FrameReading>` | Reads one written frame back and reports its size and the color its bottom row paints. |
315
- | `readCascade` | function | `() => ReadonlySet<string>` | Collects every class token the stylesheets loaded into this document actually define. |
316
- | `readClasses` | function | `(root: ParentNode) => ReadonlySet<string>` | Collects every class token the markup under one root carries. |
317
- | `readRules` | function | `() => readonly CSSRule[]` | Collects every rule the stylesheets loaded into this document hold, nested grouping rules included. |
318
- | `findRule` | function | `(selector: string) => CSSStyleRule \| undefined` | Finds the first style rule in the cascade whose selector carries a fragment. |
319
- | `findKeyframes` | function | `(name: string) => CSSKeyframesRule \| undefined` | Finds the animation the cascade declares under one name. |
320
- | `readRows` | function | `(root: ParentNode, selector: string) => readonly string[]` | Reads the normalized visible text of every element a selector matches, in document order. |
321
- | `extractOrphans` | function | `(root: ParentNode, child: string, parent: string) => readonly string[]` | Collects every element carrying a component class rendered outside the container it belongs to. |
322
- | `extractStyles` | function | `(root: ParentNode) => readonly string[]` | Collects the markup of every element carrying a non-empty `style` attribute and of every `<style>` element, in document order, `root` included in both populations when it is an `Element`. |
323
- | `readStyle` | function | `(element: Element, property: string) => string` | Reads one resolved CSS property from a real browser element. |
324
- | `readToken` | function | `(element: Element, name: string) => string` | Reads one custom property from an element's resolved style. |
325
- | `readRootToken` | function | `(name: string) => string` | Reads one custom property from the document element. |
326
- | `readPixels` | function | `(element: Element, property: string) => number` | Reads one resolved CSS length as a number of pixels. |
327
- | `expandCaptures` | function | `(states: readonly string[], variants: readonly CaptureVariant[]) => readonly string[]` | Expands a capture registry across every variant into the filenames a complete portfolio holds. |
291
+ | API | Kind | Signature | Summary |
292
+ | ----------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
293
+ | `resolveAccessible` | function | `(name: string) => HTMLElement` / `(role: string, name: string) => HTMLElement` | Resolves one visible, focus-reachable interactive element by its exact accessible name. A wholly-off-viewport target is scrolled into view before reachability is measured. |
294
+ | `resolveRendered` | function | `(first: string, second?: string) => HTMLElement` | Resolves one rendered, focus-reachable interactive element without requiring it to intersect the viewport yet. |
295
+ | `computeNamePattern` | function | `(name: string) => RegExp` | Computes the pattern that matches one accessible name a decorative glyph may sit beside. |
296
+ | `isOutsideViewport` | function | `(rectangle: DOMRectReadOnly) => boolean` | Determines whether a rectangle lies wholly outside the browser viewport. |
297
+ | `isRendered` | function | `(element: Element) => boolean` | Determines whether the accessibility tree presents one element at all. |
298
+ | `isReachable` | function | `(element: Element) => boolean` | Determines whether a person can click one element where it sits. |
299
+ | `readHit` | function | `(element: Element) => Element \| undefined` | Reads the topmost element at one element's bounding-box centre. |
300
+ | `clickAccessible` | function | `(name: string) => Promise<void>` / `(role: string, name: string) => Promise<void>` | Clicks one visible, focus-reachable control by its accessible name through the browser provider. |
301
+ | `clickAccessibleWithin` | function | `(region: string, role: string, name: string) => Promise<void>` | Clicks one human-reachable control by role and accessible-name text inside a named region. |
302
+ | `clickDisclosure` | function | `(name: string) => Promise<void>` | Opens or closes one native details disclosure by its rendered summary. |
303
+ | `typeAccessible` | function | `(name: string, text: string) => Promise<void>` | Replaces a named field's value through focus, select-all, deletion, and real keystrokes. |
304
+ | `fillAccessible` | function | `(name: string, text: string) => Promise<void>` | Replaces a named field's value in one operation, for text too long to type key by key. |
305
+ | `traverseAccessible` | function | `(name: string) => Promise<HTMLElement>` | Reaches a named control only through natural forward Tab traversal from the current focus. |
306
+ | `pressKeys` | function | `(keys: string) => Promise<void>` | Sends a key sequence to whatever holds focus, and refuses to send it to nothing. |
307
+ | `readPerception` | function | `(name: string) => string` | Reads the normalized visible text of one named region, dialog, table, tab panel, or alert. |
308
+ | `readPage` | function | `() => string` | Reads the normalized visible text of the whole page. |
309
+ | `readFocus` | function | `() => string \| undefined` | Reads the rendered text of the element that holds focus. |
310
+ | `readValue` | function | `(role: string, name: string) => string` | Reads the value a resolved control renders. |
311
+ | `readRefusal` | function | `(name: string) => string \| undefined` / `(role: string, name: string) => string \| undefined` | Reads the refusal one named target answers with, or nothing when it resolves. |
312
+ | `readText` | function | `(element: Element) => string` | Reads one element's rendered text the way a name computation reads it. |
313
+ | `readRole` | function | `(element: Element) => string \| undefined` | Reads the role one element carries in the accessibility tree. |
314
+ | `readName` | function | `(element: Element) => string` | Reads the accessible name one element is announced under. |
315
+ | `readStates` | function | `(element: Element) => readonly string[]` | Reads the states one element is announced in. |
316
+ | `describeTree` | function | `(element: Element) => string` | Describes the accessible tree one rendered element presents. |
317
+ | `describeFocus` | function | `(element: Element) => string` | Describes the order sequential keyboard navigation visits one element's controls in. |
318
+ | `waitForFrame` | function | `() => Promise<void>` | Waits for one animation frame to settle pending browser paint work. |
319
+ | `waitForState` | function | `(name, state, options?) => Promise<readonly string[]>` / `(role, name, state, options?) => Promise<readonly string[]>` | Waits until one named control announces a state, or stops announcing it. |
320
+ | `waitForAnimations` | function | `(element: Element, options?: WaitOptions) => Promise<void>` | Waits until every finite animation on one element and its subtree has stopped moving. |
321
+ | `build` | function | `<K extends keyof HTMLElementTagNameMap>(tag: K, options?: ElementOptions) => HTMLElementTagNameMap[K]` | Builds one unmounted element of a known tag, wearing the classes, text, and attributes asked for. |
322
+ | `mount` | function | `<T extends Element>(element: T) => T` | Puts one element into the document and hands it straight back. |
323
+ | `render` | function | `(markup: string) => HTMLDivElement` / `(tag: K, classes: string) => HTMLElementTagNameMap[K]` | Renders one fixture into the document from trusted markup. |
324
+ | `typeInput` | function | `(element: HTMLInputElement \| HTMLTextAreaElement, text: string) => void` | Sets one field's value and announces it the way typing into the field does. |
325
+ | `commitInput` | function | `(element: HTMLInputElement \| HTMLTextAreaElement, text: string) => void` | Sets one field's value and commits it, the way typing and then leaving the field does. |
326
+ | `clearStorage` | function | `() => void` | Clears both browser storage surfaces. |
327
+ | `removeDatabase` | function | `(name: string) => Promise<void>` | Deletes one IndexedDB database and reports what the request actually did. |
328
+ | `parseColor` | function | `(value: string) => Color \| undefined` | Parses one computed CSS color value into straight sRGB channels. |
329
+ | `parseCSSColor` | function | `(value: string) => Color \| undefined` | Resolves any CSS color expression to straight sRGB channels, by asking the browser. |
330
+ | `matchesColor` | function | `(first: string \| Color, second: string \| Color) => boolean` | Determines whether two colors render the same, within the rounding a browser does. |
331
+ | `blendColor` | function | `(front: Color, back: Color) => Color` | Composites one color over another. |
332
+ | `measureLuminance` | function | `(color: Color) => number` | Measures one opaque color's WCAG relative luminance. |
333
+ | `measureContrast` | function | `(front: Color, back: Color) => number` | Measures the WCAG 2.x contrast ratio between two opaque colors. |
334
+ | `readLayers` | function | `(element: Element) => readonly Color[]` | Collects the painted layers standing between one element and the surface it sits on. |
335
+ | `readBackdrop` | function | `(element: Element, floor: Color) => Color` | Resolves the opaque color standing behind one element. |
336
+ | `readContrast` | function | `(element: Element, floor?: Color) => number` | Measures the WCAG 2.x contrast ratio between an element's computed text and background colors. |
337
+ | `readRing` | function | `(control: Element, worn?: Element) => number \| undefined` | Measures the contrast the focus chrome painted on one control reaches against its own backdrop. |
338
+ | `measureContent` | function | `() => number` | Measures the row the document's own content ends on, in document coordinates. |
339
+ | `stagePane` | function | `(width: number, height: number) => Promise<void>` | Sets the tester's viewport and renders the runner's pane at the size that viewport claims. |
340
+ | `releasePane` | function | `() => Promise<void>` | Hands the tester pane back to the runner's own layout, at the viewport it had before staging. |
341
+ | `captureFrame` | function | `(options: FrameOptions) => Promise<string>` | Shoots one frame at one viewport size and proves the file on disk holds this run's bytes. |
342
+ | `readFrame` | function | `(path: string) => Promise<FrameReading>` | Reads one written frame back and reports its size and the color its bottom row paints. |
343
+ | `readCascade` | function | `() => ReadonlySet<string>` | Collects every class token the stylesheets loaded into this document actually define. |
344
+ | `readClasses` | function | `(root: ParentNode) => ReadonlySet<string>` | Collects every class token the markup under one root carries. |
345
+ | `readCensus` | function | `(root: ParentNode) => CensusReading` | Takes the authored-class census of one subtree against the cascade this document loaded. |
346
+ | `readRules` | function | `() => readonly CSSRule[]` | Collects every rule the stylesheets loaded into this document hold, nested grouping rules included. |
347
+ | `findRule` | function | `(selector: string) => CSSStyleRule \| undefined` | Finds the first style rule in the cascade whose selector carries a fragment. |
348
+ | `findKeyframes` | function | `(name: string) => CSSKeyframesRule \| undefined` | Finds the animation the cascade declares under one name. |
349
+ | `readRows` | function | `(root: ParentNode, selector: string) => readonly string[]` | Reads the normalized visible text of every element a selector matches, in document order. |
350
+ | `extractOrphans` | function | `(root: ParentNode, child: string, parent: string) => readonly string[]` | Collects every element carrying a component class rendered outside the container it belongs to. |
351
+ | `extractStyles` | function | `(root: ParentNode) => readonly string[]` | Collects the markup of every element carrying a non-empty `style` attribute and of every `<style>` element, in document order, `root` included in both populations when it is an `Element`. |
352
+ | `readStyle` | function | `(element: Element, property: string) => string` | Reads one resolved CSS property from a real browser element. |
353
+ | `readToken` | function | `(element: Element, name: string) => string` | Reads one custom property from an element's resolved style. |
354
+ | `readRootToken` | function | `(name: string) => string` | Reads one custom property from the document element. |
355
+ | `readPixels` | function | `(element: Element, property: string) => number` | Reads one resolved CSS length as a number of pixels. |
356
+ | `expandCaptures` | function | `(states: readonly string[], variants: readonly CaptureVariant[]) => readonly string[]` | Expands a capture registry across every variant into the filenames a complete portfolio holds. |
357
+ | `buildDenial` | function | `(operation: string, key?: string) => DOMException` | Builds the refusal a host withholding a storage operation raises. |
358
+ | `buildContrast` | function | `(bar: number) => ContrastFixture` | Builds a detached translucent stack whose composited and flat contrast readings straddle one bar. |
359
+ | `buildEscapes` | function | `(permitted: string) => EscapeFixture` | Builds detached markup carrying one style escape of each kind, plus the sheet a project allows. |
360
+ | `buildCensus` | function | `() => CensusFixture` | Builds detached markup carrying one undeclared class token on HTML and another on SVG. |
328
361
 
329
362
  #### Factories
330
363
 
@@ -335,6 +368,8 @@ A `Shape` cell holds the constant's declared type.
335
368
  | `createPortfolio` | function | `(options: PortfolioOptions) => PortfolioInterface` | Creates the capture portfolio one run places its screenshots through. |
336
369
  | `createChannel` | function | `(name: string, output: string[], forward: (...data: unknown[]) => void) => (...data: unknown[]) => void` | Creates one console channel that records every call it receives and hands that call on unchanged. |
337
370
  | `createJournal` | function | `() => JournalInterface` | Creates the journal one scenario records its steps and the page's own output into. |
371
+ | `createStorage` | function | `(options?: StorageOptions) => WebStorageInterface` | Creates an inert `Storage` a host can withhold, grant, and run out of room in. |
372
+ | `createHarness` | function | `(options: HarnessOptions<TState, TEvent, TContext>) => HarnessInterface` | Creates a mounted statechart harness that renders one transition table and drives it row by row. |
338
373
 
339
374
  `resolveAccessible` counts a match as reachable only when every condition holds: it is connected; it
340
375
  passes a visibility check honouring opacity and CSS; its box has non-zero width and height; its
@@ -834,6 +869,36 @@ earlier [Surface](#surface) rows.
834
869
  | `stop` | `void` | Stops recording and hands every intercepted console channel back by identity. |
835
870
  | `record` | `void` | Records one step, when the journal is started. |
836
871
 
872
+ #### `WebStorageInterface`
873
+
874
+ | Method | Returns | Summary |
875
+ | -------- | ------- | ---------------------------------------------------------------------------- |
876
+ | `permit` | `void` | Grants the reads and the writes the host withheld, and replenishes no quota. |
877
+
878
+ The rest of the surface is the platform's: `length`, `key`, `getItem`, `setItem`, `removeItem`, and
879
+ `clear` are declared by the host `Storage` interface this one extends. A store created here answers
880
+ through those methods and intercepts no named-property access, so drive a consumer under test
881
+ through `getItem` and `setItem`;
882
+ [Bounds a shipped helper carries](#bounds-a-shipped-helper-carries) states what the property form
883
+ reads instead.
884
+
885
+ #### `HarnessInterface`
886
+
887
+ | Method | Returns | Summary |
888
+ | --------- | --------------- | ------------------------------------------------------------------------ |
889
+ | `execute` | `Promise<void>` | Drives every row in table order, from a fresh tally and a cleared state. |
890
+ | `destroy` | `void` | Removes the mounted root, and does nothing when it is already removed. |
891
+
892
+ `execute` drives the table the harness was constructed with, so a second call re-runs the same rows.
893
+ It clears every rendered `result`, the rendered `state`, and the tally before the first row starts,
894
+ and that reset is readable while the run is in flight: a harness mid-re-run reports nothing passed,
895
+ nothing failed, and no state rather than what the run before it left. Every exit writes a terminal
896
+ status, because the gate polling the markup has no rejection channel to read: a run that completes
897
+ writes `passed` or `failed`, and a run a `state` reader ends writes `failed` and then rejects with
898
+ that reader's value by identity, without counting the row as failed. `destroy` takes the root out of
899
+ the document and leaves the element itself intact, so the tally a finished run published is still
900
+ readable from the object afterwards.
901
+
837
902
  #### `ScratchInterface`
838
903
 
839
904
  | Method | Returns | Summary |
@@ -926,43 +991,71 @@ what the link reaches, not on what it stores.
926
991
  Every message `src/browser` throws. Keep them distinct: a journey asserts the one it means, and
927
992
  absent, present-but-gated, and ambiguous are different findings about an interface.
928
993
 
929
- | Voice | Thrown by |
930
- | ------------------------------------------------------------------------------------- | ----------------------- |
931
- | `No interactive element has the accessible name "<name>"` | `resolveRendered` |
932
- | `Interactive target "<name>" is not visible and focus-reachable` | `resolveRendered` |
933
- | `Interactive target "<name>" is ambiguous across <n> elements` | `resolveRendered` |
934
- | `Interactive target "<name>" could not be resolved` | `resolveRendered` |
935
- | `Interactive target "<name>" is unreachable after scrolling` | `resolveAccessible` |
936
- | `Interactive target "<name>" is not reachable inside "<region>"` | `clickAccessibleWithin` |
937
- | `Interactive target "<name>" is ambiguous across <n> elements inside "<region>"` | `clickAccessibleWithin` |
938
- | `Interactive target "<name>" could not be resolved inside "<region>"` | `clickAccessibleWithin` |
939
- | `Native disclosure "<name>" is not visible and focus-reachable` | `clickDisclosure` |
940
- | `Native disclosure "<name>" is ambiguous across <n> elements` | `clickDisclosure` |
941
- | `Native disclosure "<name>" could not be resolved` | `clickDisclosure` |
942
- | `Interactive target "<name>" is not reachable through forward Tab traversal: <trail>` | `traverseAccessible` |
943
- | `Named region "<name>" is not visible` | `readPerception` |
944
- | `Named region "<name>" is ambiguous across <n> elements` | `readPerception` |
945
- | `Named region "<name>" could not be resolved` | `readPerception` |
946
- | `Interactive target "<name>" does not carry a value` | `readValue` |
947
- | `Computed foreground color is unavailable` | `readContrast` |
948
- | `Computed background color is unavailable` | `readContrast` |
949
- | `Tester pane is unavailable for a capture` | `stagePane` |
950
- | `Tester pane rendered <w>x<h> for a <w>x<h> viewport` | `stagePane` |
951
- | `Capture frame at <path> never settled after <n> restagings: <h> over a <h> pane` | `captureFrame` |
952
- | `Capture frame was written to <path> where <path> was asked for` | `captureFrame` |
953
- | `Capture frame at <path> is not the one this run shot` | `captureFrame` |
954
- | `Capture frame at <path> could not be read` | `readFrame` |
955
- | `Capture frame at <path> is not an image this browser decodes` | `readFrame` |
956
- | `Capture frame at <path> cannot be measured without a 2D canvas` | `readFrame` |
957
- | `Capture variant "<name>" is not registered` | `createPortfolio` |
958
- | `Capture state "<state>" is not registered` | `place` |
959
- | `Capture state "<state>" is already placed` | `place` |
960
- | `IndexedDB database "<name>" could not be deleted` | `removeDatabase` |
961
- | `IndexedDB database "<name>" is blocked by an open connection` | `removeDatabase` |
994
+ | Voice | Thrown by |
995
+ | ---------------------------------------------------------------------------------------- | ----------------------- |
996
+ | `No interactive element has the accessible name "<name>"` | `resolveRendered` |
997
+ | `Interactive target "<name>" is not visible and focus-reachable` | `resolveRendered` |
998
+ | `Interactive target "<name>" is ambiguous across <n> elements` | `resolveRendered` |
999
+ | `Interactive target "<name>" could not be resolved` | `resolveRendered` |
1000
+ | `Interactive target "<name>" is unreachable after scrolling` | `resolveAccessible` |
1001
+ | `Interactive target "<name>" is not reachable inside "<region>"` | `clickAccessibleWithin` |
1002
+ | `Interactive target "<name>" is ambiguous across <n> elements inside "<region>"` | `clickAccessibleWithin` |
1003
+ | `Interactive target "<name>" could not be resolved inside "<region>"` | `clickAccessibleWithin` |
1004
+ | `Native disclosure "<name>" is not visible and focus-reachable` | `clickDisclosure` |
1005
+ | `Native disclosure "<name>" is ambiguous across <n> elements` | `clickDisclosure` |
1006
+ | `Native disclosure "<name>" could not be resolved` | `clickDisclosure` |
1007
+ | `Interactive target "<name>" is not reachable through forward Tab traversal: <trail>` | `traverseAccessible` |
1008
+ | `Named region "<name>" is not visible` | `readPerception` |
1009
+ | `Named region "<name>" is ambiguous across <n> elements` | `readPerception` |
1010
+ | `Named region "<name>" could not be resolved` | `readPerception` |
1011
+ | `Interactive target "<name>" does not carry a value` | `readValue` |
1012
+ | `Computed foreground color is unavailable` | `readContrast` |
1013
+ | `Computed background color is unavailable` | `readContrast` |
1014
+ | `Tester pane is unavailable for a capture` | `stagePane` |
1015
+ | `Tester pane rendered <w>x<h> for a <w>x<h> viewport` | `stagePane` |
1016
+ | `Capture frame at <path> never settled after <n> restagings: <h> over a <h> pane` | `captureFrame` |
1017
+ | `Capture frame was written to <path> where <path> was asked for` | `captureFrame` |
1018
+ | `Capture frame at <path> is not the one this run shot` | `captureFrame` |
1019
+ | `Capture frame at <path> could not be read` | `readFrame` |
1020
+ | `Capture frame at <path> is not an image this browser decodes` | `readFrame` |
1021
+ | `Capture frame at <path> cannot be measured without a 2D canvas` | `readFrame` |
1022
+ | `Capture variant "<name>" is not registered` | `createPortfolio` |
1023
+ | `Capture state "<state>" is not registered` | `place` |
1024
+ | `Capture state "<state>" is already placed` | `place` |
1025
+ | `IndexedDB database "<name>" could not be deleted` | `removeDatabase` |
1026
+ | `IndexedDB database "<name>" is blocked by an open connection` | `removeDatabase` |
1027
+ | `Key sequence "<keys>" was sent with nothing focused` | `pressKeys` |
1028
+ | `Condition "<subject>" did not hold within <n>ms (waited <n>ms) (last states: <states>)` | `waitForState` |
1029
+ | `Animation subject is not connected` | `waitForAnimations` |
1030
+ | `Animation "<subject>" did not settle within <n>ms (waited <n>ms): <animations>` | `waitForAnimations` |
1031
+ | `Class census walked no element` | `readCensus` |
1032
+ | `Contrast control cannot straddle the bar <bar>` | `buildContrast` |
1033
+ | `Access is denied for <operation> "<key>"` | `buildDenial` |
1034
+ | `No room is left for <key>` | `createStorage` |
1035
+ | `Storage quota must be a non-negative integer` | `createStorage` |
1036
+ | `Statechart harness mounted no transition` | `createHarness` |
1037
+ | `Statechart harness carries no status` | `status` |
1038
+
1039
+ Some of those rows are not plain `Error` messages. `buildDenial` returns a `DOMException` named
1040
+ `SecurityError`, `createStorage` raises that one from every operation the permission withholds and a
1041
+ `DOMException` named `QuotaExceededError` from a write past the quota, and each carries the name a
1042
+ denied or full origin carries. Assert on the `name` as well as on the message.
1043
+
1044
+ The `waitForState` and `waitForAnimations` rows are the wait family's own voices with a subject
1045
+ this layer supplies. `waitForState`
1046
+ names the control and the state it was waiting for as its condition's description, and appends the
1047
+ states it last read, so an exhausted wait says what the control was announcing instead.
1048
+ `waitForAnimations` names the subject through `readRole` and `readName` and lists the animations
1049
+ still running. Both validate their bounds through `checkBounds`, which raises
1050
+ `Wait budget must be finite and non-negative` for the first and `Animation budget must be finite and
1051
+ non-negative` for the second.
962
1052
 
963
1053
  Some of them are narrowing rather than findings, and no input reaches them. Each `could not be
964
1054
  resolved` is one: a preceding length check does not narrow the later lookup under
965
- `noUncheckedIndexedAccess`, so the branch gives the value its type.
1055
+ `noUncheckedIndexedAccess`, so the branch gives the value its type. `Statechart harness carries no
1056
+ status` is another: the harness writes that attribute at construction and nothing but the harness
1057
+ writes it, so the reading is a member of `STATECHART_STATUSES` unless a caller took the attribute
1058
+ off the root it was handed.
966
1059
 
967
1060
  The capture guards are the other population no test drives, because each answers for a runner or a
968
1061
  provider this package does not control. `Tester pane is unavailable for a capture` fires where
@@ -1140,9 +1233,17 @@ These hold across `src/core`, `src/browser`, `src/server`, and this guide.
1140
1233
  fixture looking alive. `remove` is written out for the opposite reason: `rmSync` with `force`
1141
1234
  does not throw on a path that is not there, so without the check it would report success against
1142
1235
  a fixture that is gone.
1143
- 9. **Zero runtime dependencies, and no foreign type in a signature.** `dependencies` is empty and
1144
- stays empty. No exported signature names an `@orkestrel/*` type, so no consumer can be handed a
1145
- two-copies type failure by installing this package.
1236
+ 9. **One runtime dependency, and no foreign type in a signature.** `dependencies` holds exactly
1237
+ `@orkestrel/contract`: `src/core` reads its `Result` inside `retryUntil` and narrows with its
1238
+ guards, `src/browser` narrows with them too, and `src/server` narrows and parses with them. The
1239
+ [Limits](#limits) row `An outcome triple` records that adoption and why a second copy of the type
1240
+ was refused. `vitest` is a peer dependency rather than a runtime one, so the runner a consumer
1241
+ already installed is the one this package drives. No exported signature names a type from another
1242
+ `@orkestrel` package, so no consumer can be handed a two-copies type failure by installing this
1243
+ package. The browser entry's declarations do name this package's own core types — `CaptureVariant`
1244
+ extends `JourneyVariant`, and `StateOptions` extends `WaitOptions` — and the declaration roll-up
1245
+ writes those as imports from `@orkestrel/test`. That is one package resolving its own root entry
1246
+ rather than a second copy of anything, which is the whole of what the two-copies rule is about.
1146
1247
  10. **`createTeardown` runs newest-first, and every handler runs.** `destroy()` takes the registered
1147
1248
  handlers in reverse registration order and awaits each one before starting the next, so a
1148
1249
  handler that undoes what a later registration depends on runs after it. A handler that throws or
@@ -1154,9 +1255,10 @@ These hold across `src/core`, `src/browser`, `src/server`, and this guide.
1154
1255
  joining this one, and `count` read from inside a running handler counts only those late
1155
1256
  registrations. A repeated `destroy()` runs nothing that already ran, which is what makes it
1156
1257
  idempotent. The list registers no Vitest hook itself: the consumer writes
1157
- `afterEach(() => teardown.destroy())` once, in its own setup. That one line is the price of the
1158
- zero-dependency contract, because registering the hook here would take a runtime dependency on
1159
- the test runner and the zero-runtime-dependencies contract forbids one.
1258
+ `afterEach(() => teardown.destroy())` once, in its own setup. That one line is what keeps the
1259
+ runner out of this package's `dependencies`: registering the hook here would take a runtime
1260
+ dependency on the test runner, and `vitest` is a peer dependency precisely so the installation
1261
+ the consumer already made is the one that runs.
1160
1262
  11. **`createLoopback` binds a server the caller made.** The caller constructs its own unstarted
1161
1263
  server and keeps every protocol handler on it; this package supplies the bind and the release
1162
1264
  and nothing else. It listens on port `0` at `127.0.0.1`, so the host assigns the port and the
@@ -1187,9 +1289,12 @@ These hold across `src/core`, `src/browser`, `src/server`, and this guide.
1187
1289
  both, `clearStorage` takes nothing at all, and `removeDatabase` takes a database name. The
1188
1290
  predicates, the element readers, and the describers do take a node —
1189
1291
  `isRendered`, `isReachable`, `readHit`, `readText`, `readRole`, `readName`, `readStates`,
1190
- `describeTree`, `describeFocus`, `extractOrphans`, `readRows`, `readStyle`, `readToken`,
1191
- `readPixels`, `readContrast`, `readLayers`, `readBackdrop`, and `readRing` — and each is a
1192
- reader of a node the caller already has rather than a verb that acts on a target. `captureFrame`
1292
+ `readCensus`, `describeTree`, `describeFocus`, `extractOrphans`, `readRows`, `readStyle`,
1293
+ `readToken`, `readPixels`, `readContrast`, `readLayers`, `readBackdrop`, and `readRing` — and
1294
+ each is a reader of a node the caller already has rather than a verb that acts on a target.
1295
+ `waitForAnimations` takes one as the subject of a wait, and waiting for a browser to stop
1296
+ painting it changes nothing about it. `buildContrast`, `buildEscapes`, and `buildCensus` return
1297
+ detached nodes for the caller to append and remove, the way `build` does. `captureFrame`
1193
1298
  and `place` take one as the subject of a photograph, which is a reading too: neither moves
1194
1299
  focus, dispatches an event, nor changes what the element renders. `typeInput` and `commitInput`
1195
1300
  are the one pair that acts on the element it is handed, and the exception is deliberately
@@ -1197,13 +1302,15 @@ These hold across `src/core`, `src/browser`, `src/server`, and this guide.
1197
1302
  `input` and a test that already holds the field. Drive the field by name wherever the keystrokes
1198
1303
  are part of what the journey claims. `readRing` is the case that makes the split explicit. It
1199
1304
  measures the focus chrome a browser painted and never brings the focus about, so a journey
1200
- reaches the control through `traverseAccessible` or `userEvent.keyboard` from `vitest/browser`
1201
- and then measures what landed. The whole environment imports `vitest/browser` and DOM globals
1202
- and nothing else — no `src/core` import, no framework, no `node:*`, and no `import.meta.env`, so
1203
- whether a run writes captures is the consumer's decision through `PortfolioOptions.enabled`
1204
- rather than an environment variable this package reads. `vitest` is a peer dependency, so the
1205
- provider the layer drives is the one the consumer already installed, and the
1206
- zero-runtime-dependencies contract's empty `dependencies` is untouched.
1305
+ reaches the control through `traverseAccessible` or `pressKeys` and then measures what landed.
1306
+ The environment imports `vitest/browser`, DOM globals, this package's own core, and the
1307
+ `@orkestrel/contract` guards it narrows with — and no framework, no `node:*`, and no
1308
+ `import.meta.env`, so whether a run writes captures is the consumer's decision through
1309
+ `PortfolioOptions.enabled` rather than an environment variable this package reads. The core
1310
+ import ships as an import rather than as a second copy: the browser build declares `@src/core`
1311
+ external and rewrites it to the core entry beside it, and the declaration roll-up rewrites it to
1312
+ the package name, so a consumer resolves one `waitForCondition` rather than two. `vitest` is a
1313
+ peer dependency, so the provider the layer drives is the one the consumer already installed.
1207
1314
  14. **The wait family polls only where nothing publishes an event.** The no-polling architecture law
1208
1315
  governs a product's idle wakeup: a running system parks on the event or the abort signal that
1209
1316
  fires. A test instrument is the other case. It waits on a fact another process produces — a file
@@ -1212,7 +1319,15 @@ These hold across `src/core`, `src/browser`, `src/server`, and this guide.
1212
1319
  budget measured with `performance.now()`. Where an event does exist, `waitForEvent` is the door:
1213
1320
  it parks on the subscription, validates the interval for consistency with the family and never
1214
1321
  uses it, and invokes the cleanup the subscriber returned on timeout, on abort, and on delivery
1215
- alike. `waitForCondition`, `retryUntil`, and `waitForEvent` each name what they are waiting for,
1322
+ alike. `waitForAnimations` is the browser environment's parking door, on the same terms: it parks
1323
+ on each animation's own `finished` promise and validates the interval for consistency with the
1324
+ family without ever using it. `waitForText` polls, because a reading of text publishes no event,
1325
+ and it refuses two calls no reading could ever satisfy before it takes one: an empty `text` or
1326
+ an empty `absent` with `Text expectation must not be empty`, because every string contains the
1327
+ empty string, and a `text` that carries `absent` with
1328
+ `Text departure must not appear in the text expectation`, because a reading that satisfies the
1329
+ arrival carries the departure too — under `exact` it equals `text` and without it contains
1330
+ `text`. `waitForCondition`, `retryUntil`, and `waitForEvent` each name what they are waiting for,
1216
1331
  and that description is what the timeout message carries — a wait nobody described times out
1217
1332
  saying nothing about what failed. Every bound is validated finite and non-negative before
1218
1333
  anything is read, a budget of `0` still permits the immediate first reading, and an abort rejects
@@ -1290,6 +1405,25 @@ These hold across `src/core`, `src/browser`, `src/server`, and this guide.
1290
1405
  `releasePane` returns the tester to the viewport it held before the staging, so the variant a
1291
1406
  frame was shot at belongs to that frame alone, and a suite that wants a size of its own calls
1292
1407
  `page.viewport` rather than this pair.
1408
+ 19. **The statechart harness is test-side, and the markup is its whole contract.** A page cannot
1409
+ import this package. `@orkestrel/test` is a development dependency, its browser entry imports
1410
+ `vitest/browser` at module scope, and that import throws outside Browser Mode — so an
1411
+ application that reached for `createHarness` would be shipping the runner to production.
1412
+ The harness therefore mounts from the suite, and the only thing that crosses to a gate
1413
+ outside the page is the rendered markup. `STATECHART_ATTRIBUTES` names every attribute on
1414
+ both sides of that boundary, so neither the harness nor the gate spells a `data-statechart-*`
1415
+ string of its own. The markup is framework-free and the harness renders it with `build` and
1416
+ `mount`, so a workspace that installs no view library can still run it. Every reading the
1417
+ object publishes comes off that markup rather than out of a field beside it: `status`,
1418
+ `total`, `passed`, and `failed` read the root's attributes and `failures` reads the name of
1419
+ each row whose rendered `result` reads `failed`, so a test asserting on the object and a gate
1420
+ polling the page cannot report different things. That gate has no rejection channel, so every
1421
+ exit writes a terminal status: a completed run writes `passed` or `failed`, and a run that a
1422
+ `state` reader or a non-`Error` phase throw ends writes `failed` and then rejects with that
1423
+ value by identity, leaving the row it was reading uncounted. A run that rejected while the root
1424
+ still read `running` would strand the gate on a reading the harness never leaves. The gate stays
1425
+ outside this package: the harness carries its own tally, so nothing here reads a harness back,
1426
+ and no page is generated or published to host one.
1293
1427
 
1294
1428
  ### Threat model
1295
1429
 
@@ -1387,6 +1521,20 @@ or when a consumer appears the ruling did not consider.
1387
1521
  | Abort-signal instrumentation | Ships | It ships as `createSignal`. A recorder handed to `addEventListener('abort', …)` still records what one listener heard; what no recorder can answer is how many listeners stand on the signal at this moment, which is the question a leak asks. The instrumented signal counts its own abort registrations, keyed by the original callback and the capture mode, so a helper that removes what it added proves the removal. A registration leaves the tally on removal, on a one-shot delivery, and when a signal scoping it aborts, which is what makes the reading a live tally rather than an install count. |
1388
1522
  | An outcome triple — a produced arm, a failed arm, and their union | Adopted | This package runtime-depends on `@orkestrel/contract` and imports `Result`, `Success`, and `Failure` from it rather than shipping a second copy. No signature published here returns one — `retryUntil` reads the type internally — and the names are not re-exported. |
1389
1523
  | A statechart transition table and its runner | Ships | It ships as `StateTransition` and `StateScenario`, driven by `executeScenario` and `executeScenarios`, with `STATECHART_ATTRIBUTES` and `STATECHART_STATUSES` for the harness a browser workspace renders. `elements` and `veneer` each declare the field-identical pair of interfaces in their own setup file, so the fleet already writes this twice and a third copy drifts the moment one of them adds a phase. The runner ships in its walking form rather than its registering one: a package helper registers no test, so `describe` and `it.each` stay in the workspace and this drives whatever rows it is handed. The row's name is what a failure carries, because a table's rows run under one test name and a bare assertion message never says which row produced it. No published package declares a generic transition record or a closure-walking runner. `@orkestrel/workflow` names a task's behavior with a string and sequences structurally, so it neither takes a scenario's closures nor drives `arrange`, `act`, and `assert` in order, and adopting it would move this package off layer 0 and pull that package's whole runtime graph into every consumer's test install. `STATECHART_STATUSES` names a harness's reported run state rather than a task's derived status, so it does not restate `LifecycleStatus`. `STATECHART_ATTRIBUTES` is the fleet contract the journey skill's statechart reference fixes for every harness and every gate, so it is a mechanism the fleet shares rather than one suite's policy. |
1524
+ | A keyboard verb — `pressKeys` | Ships | It ships as `pressKeys`, and the refusal is what keeps it from being a rename of `userEvent.keyboard`. A key sent while the document body holds focus reaches no control, every assertion after it reads the surface the key never touched, and nothing in the provider's verb reports that. The refusal is an invariant rather than a spelling, and the skill stops teaching the provider's verb directly. |
1525
+ | A text wait — `waitForText` | Ships | It ships in `src/core` as `waitForText`, because a reading of text is host-independent and the reader is a parameter. `waitForCondition` is the poll underneath it and asks the caller to write the comparison; `retryUntil` repeats a real operation and counts a throw as an attempt, which is the wrong direction for a reading that must stop on a broken region. What this adds over writing the predicate by hand is the pair of bounds a replacement needs: `exact` for a reading that must be the sentence rather than carry it, and `absent` for the sentence the screen is replacing, without which a wait resolves on the frame carrying both. An empty expectation is refused rather than satisfied by every reading. |
1526
+ | An announced-state wait — `waitForState` | Ships | It ships as `waitForState`. The fleet's browser suites poll a framework's own class names to decide a menu has finished opening, which reads a stylesheet's vocabulary and goes stale when the framework renames it. This waits on what the control announces, resolves the control afresh on every reading so a re-rendered node is still the subject, and returns the states at resolution so an assertion has them. Where a surface announces nothing, the finding is the surface's: the replacement is an `aria-expanded` on the trigger rather than a helper that reads classes. |
1527
+ | An animation wait — `waitForAnimations` | Ships | It ships as `waitForAnimations`. The fleet hand-rolled the same loop over `getAnimations` in more than one workspace, and a contrast or color reading taken while paint is moving reports an interpolated frame no state of the interface paints. It parks on each animation's own `finished` promise rather than polling, re-reads after each completion so an animation a finishing one starts is waited on, and excludes an animation declaring infinite iterations — a spinner that runs forever is a finding about the reading rather than a wait to lengthen. |
1528
+ | A refusal reader — `readRefusal` | Ships | It ships as `readRefusal`. `captureError` is the bar it has to clear and it does: this fixes the resolver rather than taking any thunk, translates the `unknown` a capture hands back into `string \| undefined`, and rethrows what is not an `Error` instead of returning it as a message. A journey asserting that a control is gated rather than absent compares the exact sentence, and `roughnotes` writes that same capture inline wherever it asserts on a refusal. |
1529
+ | A storage fixture — `createStorage` | Ships | It ships as `createStorage`, returning `WebStorageInterface`. A consumer declared a class bounding writes and a class withholding permission, and each is the same inert store under different options. It is a real `Storage` backed by a map of its own, it patches neither browser surface, and `permit` grants what the host withheld. A stalled read was refused with it: `Storage` is synchronous, so a stall is not expressible against the interface a consumer codes to. |
1530
+ | An authored-class census — `readCensus` | Ships | It ships as `readCensus`. `readClasses` differenced against `readCascade` is the check, and every workspace writes that difference the same way; what each of them omits is the population, so a walk that read nothing reports the same empty difference as markup whose every class the cascade declares. Reporting `elements` beside `undeclared` and refusing an empty walk is the invariant this adds over the two readings it composes. |
1531
+ | Control fixture builders — `buildContrast`, `buildEscapes`, `buildCensus` | Ships | Each ships. An instrument is not evidence until its control has failed, and a consumer's contrast, style-escape, and census readings each ran against fixtures that could not fail them: every other fixture painted its own opaque background, so the compositing walk never ran; the escape reading was fed an inline attribute and never a `<style>` element; and the census was fed an HTML token and never the SVG one whose class list is no string. Each builder is parameterized by what the policy owns — the bar, the exempt id — and returns detached nodes, so the caller decides where they are read and nothing is mounted for it. |
1532
+ | A stalled-read store | Refused | A store whose reads hang is not expressible against the interface a consumer codes to: `Storage` is synchronous, so `getItem` either answers or throws and there is no point at which a caller awaits it. A test that needs a hanging read needs an asynchronous surface, which is a different subject from the Web Storage one `createStorage` stands in for. |
1533
+ | A painted-population predicate — `isPainted` | Refused | The platform already answers it. `element.checkVisibility()` reports what the box tree renders and a non-zero `getBoundingClientRect()` reports what occupies space, and `isRendered` and `isReachable` already compose those two for the questions this layer asks. A third predicate over the same readings adds a name rather than an invariant. |
1534
+ | A statechart harness — `createHarness` | Ships | It ships as `createHarness`, with `HarnessOptions` and `HarnessInterface` beside it. The attribute contract is one every consumer would otherwise implement identically, which is the same admission that shipped the journey layer: `STATECHART_ATTRIBUTES` already published the names, and a workspace writing its own renderer against them writes a slightly different root, a slightly different row, and a gate that reads one workspace's markup and not the next one's. It renders framework-free markup through `build` and `mount`, drives each row through the `executeScenario` this package already publishes, and carries on past a failing row so one run reports on the whole table. |
1535
+ | A separate gate reader — `readHarness` | Refused | The object already carries the tally. `createHarness` returns `status`, `total`, `passed`, `failed`, and `failures`, every one of them read off the rendered markup, so a second helper that parsed the same attributes back out would be a wrapper over `getAttribute` adding no boundary, invariant, or translation. A gate running outside this package is outside its environment too — it polls a page from a process that never imports a module importing `vitest/browser` — so what it needs is the attribute names, and `STATECHART_ATTRIBUTES` is what publishes them. |
1536
+ | A generated or published harness page | Refused | Which transitions a surface owes, where that page is deep-linked, and whether it ships to anyone are product decisions, and framework code stops before them. A page hosting a harness would also have to import this package, which rule 19 rules out: the browser entry imports `vitest/browser` at module scope. The mechanism ships and the page does not. |
1537
+ | A framework-class disclosure settle | Refused | A helper that waits for a named element to carry one class and not two others encodes one framework's transition vocabulary, which is that framework's policy rather than a mechanism. `waitForState` waits on what the control announces and `waitForAnimations` waits on the paint itself, and between them they answer the question the class poll was asked. A surface announcing nothing is the finding. |
1390
1538
 
1391
1539
  `ScratchInterface`'s own members were ruled the same way, and coherence rather than demand decided
1392
1540
  them. `ensure` ships because it is the one member that produces an empty directory — `write` always
@@ -1428,6 +1576,12 @@ the helper rather than to the host, and each names what to reach for instead.
1428
1576
  key reads `undefined` at runtime under a non-optional type and `isRecorderMapComplete` still reports
1429
1577
  `true`, because it checks the events it was given rather than the type it was keyed by. Pass a
1430
1578
  literal array or a tuple, so the element type is exactly what was listed.
1579
+ - **`createStorage` answers through its methods and intercepts no named-property access.** The store
1580
+ is a real `Storage`, and `Storage` declares an index signature, so `store.theme` typechecks with no
1581
+ cast and reads `undefined` while `getItem('theme')` answers. A property write lands on the object
1582
+ rather than in the store, so it consumes no quota, meets no withheld permission, and is invisible
1583
+ to every read. Drive the code under test through `getItem` and `setItem`, which is where the seed,
1584
+ the quota, and the grant are.
1431
1585
  - **`readProperty`'s `TypeError` names the target, never the read.** It refuses a target that is
1432
1586
  neither an object nor a function before it reads anything, and a getter that throws on an accepted
1433
1587
  target hands that throw straight to the caller. Wrap the call in `captureError` where a hostile
@@ -1436,6 +1590,19 @@ the helper rather than to the host, and each names what to reach for instead.
1436
1590
  carrying no leading number — `'auto'`, `'none'`, `''` — reads as `0`, because none of them
1437
1591
  contributes a pixel to what a reader sees, so a caller cannot tell an unparsable value from a
1438
1592
  genuine zero. Read the text with `readStyle` where that distinction is the subject.
1593
+ - **`isRendered` and `isReachable` read an ancestor attribute inside the element's own tree.** Each
1594
+ asks `closest` for the `aria-hidden` ancestor and the `[inert]` ancestor, and `closest` never
1595
+ crosses a shadow boundary, so a host carrying either attribute is invisible to a subject inside its
1596
+ shadow root — in an open root and a closed one alike. What the flat tree decides still reaches the
1597
+ subject: a host the document does not lay out takes the element off the page, and both predicates
1598
+ refuse it. Read a `true` for a shadow subject as the element's own answer, and ask the host
1599
+ separately where an ancestor attribute is the subject.
1600
+ - **`waitForAnimations` waits on the animations a browser reports as running.** A finished animation
1601
+ filling its target stays in the list and is already at rest, a paused one is at rest too and
1602
+ nothing here resumes it, and an animation declaring infinite iterations never finishes. Each is
1603
+ left out, so a wait that resolves is a claim about the paint that was moving rather than about the
1604
+ list being empty. Read `element.getAnimations({ subtree: true })` directly where the membership of
1605
+ that list is the subject.
1439
1606
  - **`readHit` answers for one point, and a node it returns is no proof of a cover.** An element the
1440
1607
  document does not render measures a zero rectangle at the origin, and a zero-area element measures
1441
1608
  a point on its own edge, so each is hit-tested like any other point and names whatever paints
@@ -1774,6 +1941,39 @@ Every bounded member takes an `AbortSignal` and rejects with the signal's own re
1774
1941
  controller ends a whole file's waits. A budget of `0` still permits the immediate first reading, and
1775
1942
  a bound that is not finite and non-negative is refused before anything is read.
1776
1943
 
1944
+ ### Wait for a sentence to arrive
1945
+
1946
+ A journey waits for what a person reads, and the reading is yours to scope: a whole page, one named
1947
+ region, or a value a host-independent test computes. A screen replacing one sentence with another
1948
+ passes through a frame carrying both, so name the departing sentence in `absent` and the wait
1949
+ resolves on the reading that carries one and not the other.
1950
+
1951
+ ```ts
1952
+ import { waitForText } from '@orkestrel/test'
1953
+
1954
+ let painted = 'Signed out'
1955
+ setTimeout(() => {
1956
+ painted = 'Signed out Signed in'
1957
+ }, 10)
1958
+ setTimeout(() => {
1959
+ painted = 'Signed in'
1960
+ }, 30)
1961
+
1962
+ await waitForText('the session line replaces the prompt', () => painted, 'Signed in', {
1963
+ absent: 'Signed out',
1964
+ budget: 2000,
1965
+ }) // 'Signed in' — the reading that satisfied the poll
1966
+
1967
+ // Throws Error: Text expectation must not be empty
1968
+ await waitForText('anything', () => painted, '')
1969
+ ```
1970
+
1971
+ `waitForCondition` owns the poll, so the bounds, the timeout voice, and the abort reason are that
1972
+ helper's, and a reader that throws stops the wait rather than counting as a reading that did not
1973
+ satisfy it. Pass `exact` where the reading must be the sentence rather than carry it. Scope the
1974
+ reading as narrowly as the claim: a wait over the whole page resolves on the sentence wherever it
1975
+ lands, which is rarely what a journey means.
1976
+
1777
1977
  ### Copy a JSON value
1778
1978
 
1779
1979
  This demonstration builds an interface-typed value, copies it through JSON serialization, and
@@ -1874,47 +2074,133 @@ expect(JSON.stringify(serializeSchema(received))).toBe(wire)
1874
2074
  A statechart table is a row per transition, and a row is the transition plus the three phases that
1875
2075
  prove it: `arrange` puts the entity into `from`, `act` applies the `event`, and `assert` reads the
1876
2076
  entity for `to`. `executeScenarios` walks the table and hands each row a context of its own; it
1877
- registers nothing, so `describe` and `it` stay where you write them. In the following fence,
1878
- `Disclosure` is the entity under test, and it is closed until something shows it.
2077
+ registers nothing, so `describe` and `it` stay where you write them.
2078
+
2079
+ The entity in the following fences is a real one, and the fences are the table this package's own
2080
+ browser suite runs. It is a native disclosure with two doors: the summary toggles it, and a Dismiss
2081
+ button closes it and does nothing when it is already closed. That second door is what gives the
2082
+ table a row whose event leaves the state where it found it, which a lone `<details>` cannot have —
2083
+ its one event always flips. Every value these fences claim is pinned in
2084
+ `tests/src/browser/factories.test.ts`, because the fences drive a browser and the `guides` project
2085
+ runs with the browser disabled.
2086
+
2087
+ The phases are module functions the whole table shares, which is the shape a table of any size
2088
+ wants: each phase reads its subject from its own parameters rather than from the row it belongs to,
2089
+ so one set of three serves every row.
1879
2090
 
1880
2091
  ```ts
1881
2092
  import type { StateScenario } from '@orkestrel/test'
1882
- import { executeScenarios } from '@orkestrel/test'
2093
+ import { executeScenarios, requireValue } from '@orkestrel/test'
2094
+ import { clickAccessible, clickDisclosure, readStates, render } from '@orkestrel/test/browser'
1883
2095
  import { expect, it } from 'vitest'
1884
2096
 
1885
2097
  type DisclosureState = 'closed' | 'open'
1886
- type DisclosureEvent = 'show' | 'hide'
2098
+ type DisclosureEvent = 'toggle' | 'dismiss'
1887
2099
 
1888
2100
  interface DisclosureContext {
1889
- readonly disclosure: Disclosure
2101
+ readonly summary: HTMLElement
2102
+ }
2103
+
2104
+ // A journey verb resolves its own target by accessible name, so two mounted disclosures called
2105
+ // "Advanced" are an ambiguity rather than a second fixture. Each build takes the previous one out.
2106
+ let mounted: HTMLElement | undefined
2107
+
2108
+ function buildDisclosure(): DisclosureContext {
2109
+ mounted?.remove()
2110
+ const container = render(
2111
+ '<details><summary>Advanced</summary><p>Every setting.</p></details><button type="button">Dismiss</button>',
2112
+ )
2113
+ const details = requireValue(container.querySelector('details'))
2114
+ requireValue(container.querySelector('button')).addEventListener('click', () => {
2115
+ details.open = false
2116
+ })
2117
+ mounted = container
2118
+ return { summary: requireValue(container.querySelector('summary')) }
2119
+ }
2120
+
2121
+ function readDisclosure(context: DisclosureContext): DisclosureState {
2122
+ return readStates(context.summary).includes('expanded') ? 'open' : 'closed'
2123
+ }
2124
+
2125
+ async function arrangeDisclosure(
2126
+ context: DisclosureContext,
2127
+ state: DisclosureState,
2128
+ ): Promise<void> {
2129
+ if (readDisclosure(context) !== state) await clickDisclosure('Advanced')
2130
+ }
2131
+
2132
+ // The context is unused because a journey verb finds what a person reads rather than a node this
2133
+ // row was handed.
2134
+ async function actOnDisclosure(_context: DisclosureContext, event: DisclosureEvent): Promise<void> {
2135
+ if (event === 'toggle') await clickDisclosure('Advanced')
2136
+ else await clickAccessible('Dismiss')
2137
+ }
2138
+
2139
+ function assertDisclosure(context: DisclosureContext, state: DisclosureState): void {
2140
+ expect(readDisclosure(context)).toBe(state)
1890
2141
  }
1891
2142
 
1892
2143
  const SCENARIOS: ReadonlyArray<StateScenario<DisclosureState, DisclosureEvent, DisclosureContext>> =
1893
2144
  [
1894
2145
  {
1895
- transition: { name: 'closed opens on show', from: 'closed', event: 'show', to: 'open' },
1896
- arrange(context, state) {
1897
- if (state === 'open') context.disclosure.show()
2146
+ transition: {
2147
+ name: 'closed opens through the summary',
2148
+ from: 'closed',
2149
+ event: 'toggle',
2150
+ to: 'open',
1898
2151
  },
1899
- act(context, event) {
1900
- if (event === 'show') context.disclosure.show()
1901
- else context.disclosure.hide()
2152
+ arrange: arrangeDisclosure,
2153
+ act: actOnDisclosure,
2154
+ assert: assertDisclosure,
2155
+ },
2156
+ {
2157
+ transition: {
2158
+ name: 'open closes through the summary',
2159
+ from: 'open',
2160
+ event: 'toggle',
2161
+ to: 'closed',
1902
2162
  },
1903
- assert(context, state) {
1904
- expect(context.disclosure.state).toBe(state)
2163
+ arrange: arrangeDisclosure,
2164
+ act: actOnDisclosure,
2165
+ assert: assertDisclosure,
2166
+ },
2167
+ {
2168
+ transition: {
2169
+ name: 'open closes through the button',
2170
+ from: 'open',
2171
+ event: 'dismiss',
2172
+ to: 'closed',
1905
2173
  },
2174
+ arrange: arrangeDisclosure,
2175
+ act: actOnDisclosure,
2176
+ assert: assertDisclosure,
2177
+ },
2178
+ {
2179
+ // The row whose event leaves the state where it found it.
2180
+ transition: {
2181
+ name: 'closed stays closed through the button',
2182
+ from: 'closed',
2183
+ event: 'dismiss',
2184
+ to: 'closed',
2185
+ },
2186
+ arrange: arrangeDisclosure,
2187
+ act: actOnDisclosure,
2188
+ assert: assertDisclosure,
1906
2189
  },
1907
- // One row per transition. Each row reuses the three phases shown earlier.
1908
2190
  ]
1909
2191
 
1910
2192
  it('walks the disclosure statechart', async () => {
1911
- await executeScenarios(SCENARIOS, () => ({ disclosure: new Disclosure() }))
2193
+ await executeScenarios(SCENARIOS, buildDisclosure)
1912
2194
  })
1913
2195
  ```
1914
2196
 
2197
+ Name each row for the door it drove. A table that names only the states reads as if one mechanism
2198
+ moved the entity, and the row that matters most here is the one where the button leaves the
2199
+ disclosure exactly as it found it — a name saying which control was pressed is what separates that
2200
+ row from the toggle rows beside it.
2201
+
1915
2202
  Both unions are the entity's own vocabulary, so a row naming a state or an event the entity does not
1916
- have fails to typecheck rather than at runtime. Each phase reads its subject from its own parameters
1917
- rather than from the row, which is what lets one set of phases serve every row in the table.
2203
+ have fails to typecheck rather than at runtime.
1918
2204
 
1919
2205
  The rows run one after another, because a statechart's rows drive one entity and a parallel run
1920
2206
  would have them arranging over each other. The run stops at the first row that fails, and the row's
@@ -1927,18 +2213,27 @@ const MISMATCHED: ReadonlyArray<
1927
2213
  StateScenario<DisclosureState, DisclosureEvent, DisclosureContext>
1928
2214
  > = [
1929
2215
  {
1930
- transition: { name: 'show leaves it closed', from: 'closed', event: 'show', to: 'closed' },
1931
- // The same three phases. Nothing about the row is malformed; the `to` state is unreachable.
2216
+ // Nothing about the row is malformed and the phases are the table's own; the `to` state is
2217
+ // the part the event cannot reach, so only `assert` can catch it.
2218
+ transition: {
2219
+ name: 'the summary leaves it closed',
2220
+ from: 'closed',
2221
+ event: 'toggle',
2222
+ to: 'closed',
2223
+ },
2224
+ arrange: arrangeDisclosure,
2225
+ act: actOnDisclosure,
2226
+ assert: assertDisclosure,
1932
2227
  },
1933
2228
  ]
1934
2229
 
1935
- await executeScenarios(MISMATCHED, () => ({ disclosure: new Disclosure() }))
1936
- // Error: show leaves it closed: expected 'open' to be 'closed'
2230
+ await executeScenarios(MISMATCHED, buildDisclosure)
2231
+ // Error: the summary leaves it closed: expected 'open' to be 'closed'
1937
2232
 
1938
2233
  await executeScenarios(MISMATCHED, () => {
1939
2234
  throw new Error('no fixture')
1940
2235
  })
1941
- // Error: show leaves it closed: build refused
2236
+ // Error: the summary leaves it closed: build refused
1942
2237
  ```
1943
2238
 
1944
2239
  Whatever the phase threw arrives as that error's `cause`, by identity, so an assertion's own detail
@@ -1947,10 +2242,61 @@ survives the renaming. A phase that throws something other than an `Error` is na
1947
2242
  builder's refusal arrives as the `cause` the same way, and the phases of the row it was building for
1948
2243
  never start.
1949
2244
 
2245
+ `buildRefusal` builds that refusal sentence, and `createHarness` announces the same one on the row
2246
+ it refused, so the runner and the harness name a refused build once rather than twice.
2247
+
2248
+ ```ts
2249
+ import { buildRefusal } from '@orkestrel/test'
2250
+
2251
+ buildRefusal('the summary leaves it closed', new Error('no fixture')).message
2252
+ // 'the summary leaves it closed: build refused'
2253
+ ```
2254
+
1950
2255
  Drive one row on its own with `executeScenario`, which takes the context rather than building it.
1951
2256
 
1952
- A harness that renders the same table in a browser publishes its progress through attributes, and
1953
- `STATECHART_ATTRIBUTES` and `STATECHART_STATUSES` are the names a gate polls from outside the page.
2257
+ `createHarness` renders that same table in a browser and drives it row by row, publishing its
2258
+ progress through the attributes a gate outside the page polls. It takes the table, the builder, and
2259
+ one reader that reports the state the entity is in.
2260
+
2261
+ ```ts
2262
+ import { STATECHART_ATTRIBUTES } from '@orkestrel/test'
2263
+ import { createHarness } from '@orkestrel/test/browser'
2264
+
2265
+ const harness = createHarness({
2266
+ scenarios: SCENARIOS,
2267
+ build: buildDisclosure,
2268
+ state: readDisclosure,
2269
+ })
2270
+
2271
+ harness.status // 'idle' — mounted, nothing run yet
2272
+ harness.total // 4
2273
+
2274
+ await harness.execute()
2275
+
2276
+ harness.status // 'passed'
2277
+ harness.passed // 4
2278
+ harness.failed // 0
2279
+ harness.failures // []
2280
+
2281
+ // The object reads its own markup, so a gate polling the page and a test asserting on the object
2282
+ // cannot disagree.
2283
+ harness.root.getAttribute(STATECHART_ATTRIBUTES.status) // 'passed'
2284
+ harness.root.getAttribute(STATECHART_ATTRIBUTES.total) // '4'
2285
+
2286
+ harness.destroy()
2287
+ ```
2288
+
2289
+ The harness writes the attributes onto its own markup: `status`, `passed`, `failed`, and `total` on
2290
+ its root, `scenario` and `result` on each row, `state` on the element rendering the entity's current
2291
+ state. A `role="status"` announcer narrates each step in a sentence beside them, so the page reads
2292
+ as a report rather than as a grid of attributes. A gate reads the root until `status` reads `passed`
2293
+ or `failed`, then reads the tally and names each row whose `result` reads `failed`. That reading
2294
+ always arrives, because every exit writes it: a run a `state` reader ends writes `failed` before it
2295
+ rejects, so the gate is never left polling a `running` the harness does not leave. Neither side
2296
+ spells a `data-statechart-*` string of its own, so the two cannot drift apart.
2297
+
2298
+ `STATECHART_ATTRIBUTES` and `STATECHART_STATUSES` publish those names and those readings, and
2299
+ `StatechartStatus` is the same set of readings as a named union.
1954
2300
 
1955
2301
  ```ts
1956
2302
  import { STATECHART_ATTRIBUTES, STATECHART_STATUSES } from '@orkestrel/test'
@@ -1958,15 +2304,23 @@ import { STATECHART_ATTRIBUTES, STATECHART_STATUSES } from '@orkestrel/test'
1958
2304
  STATECHART_ATTRIBUTES.status // 'data-statechart-status'
1959
2305
  STATECHART_ATTRIBUTES.scenario // 'data-statechart-scenario'
1960
2306
 
1961
- STATECHART_STATUSES[0] // 'pending' — carried until a run has a result for every row
2307
+ STATECHART_STATUSES[0] // 'pending' — carried until every declared row has rendered
1962
2308
  STATECHART_STATUSES.includes('running') // true
1963
2309
  ```
1964
2310
 
1965
- The harness writes the attributes onto its own markup: `status`, `passed`, `failed`, and `total` on
1966
- its root, `scenario` and `result` on each row, `state` on the element rendering the entity's current
1967
- state. A gate reads the root until `status` reads `passed` or `failed`, then reads the tally and
1968
- names each row whose `result` reads `failed`. Neither side spells a `data-statechart-*` string of
1969
- its own, so the two cannot drift apart.
2311
+ A run walks the tuple in the order it is written. `pending` covers construction, so a gate that
2312
+ reads it has found a harness whose rows never mounted; `idle` is a mounted harness with its tally at
2313
+ zero; `running` is a run in flight; and `passed` and `failed` are the pair a gate waits for rather
2314
+ than waiting a fixed duration. A run that a `state` reader ends writes `failed` and then rejects
2315
+ with that reader's value by identity, so the pair covers an exceptional exit as well as a completed
2316
+ one, and the row that reader was called for is not counted as failed.
2317
+
2318
+ `execute` carries on past a failing row, which is where the harness parts company with
2319
+ `executeScenarios`: one run reports on the whole table rather than stopping at the first finding,
2320
+ and a builder that refuses fails its own row under `buildRefusal`'s sentence, the one that runner
2321
+ raises. What decides whether a row's phases run is whether its builder returned rather than what it
2322
+ returned, so a table whose context is `undefined` drives every phase of every row. Call `execute`
2323
+ again to re-run the same table from a fresh tally and a cleared state.
1970
2324
 
1971
2325
  ### Read a source inventory
1972
2326
 
@@ -2361,6 +2715,89 @@ await traverseAccessible('Evaluate')
2361
2715
  readPerception('Run') // one visible named region, whitespace collapsed, hidden-but-read text kept
2362
2716
  ```
2363
2717
 
2718
+ ### Send a key to what holds focus
2719
+
2720
+ Bring focus about through a verb, then send the sequence. A key sent while the document body holds
2721
+ focus reaches no control, and every assertion after it reads the surface the key never touched, so
2722
+ that case is refused rather than sent.
2723
+
2724
+ ```ts
2725
+ import { pressKeys, traverseAccessible } from '@orkestrel/test/browser'
2726
+
2727
+ await traverseAccessible('Evaluate')
2728
+ await pressKeys('{Enter}') // the key reaches the control focus landed on
2729
+
2730
+ if (document.activeElement instanceof HTMLElement) document.activeElement.blur()
2731
+
2732
+ // Throws Error: Key sequence "{Escape}" was sent with nothing focused
2733
+ await pressKeys('{Escape}')
2734
+ ```
2735
+
2736
+ Escaping is yours, because the sequence is the subject: `{` opens a key name and `[` opens a code
2737
+ name. Reach for `typeAccessible` where the text is the subject and the key syntax is in the way.
2738
+
2739
+ ### Wait for what a control announces
2740
+
2741
+ Wait on what a person is told, not on the class names a stylesheet happens to use. `waitForState`
2742
+ resolves the control afresh on every reading, so a re-rendered node is still the subject, and
2743
+ returns the states it read at resolution.
2744
+
2745
+ ```ts
2746
+ import { clickAccessible, waitForState } from '@orkestrel/test/browser'
2747
+
2748
+ await clickAccessible('Pin note')
2749
+ await waitForState('Pin note', 'pressed=true') // ['pressed=true']
2750
+
2751
+ // The other direction, for a state that has to go away.
2752
+ await clickAccessible('Filters')
2753
+ await waitForState('button', 'Filters', 'expanded', { absent: true }) // ['collapsed']
2754
+ ```
2755
+
2756
+ Spell the state the way `readStates` reports it. The exhaustion message names the control, the
2757
+ state, and the states last read, so a wait that ran out says what the control was announcing
2758
+ instead. Where the surface announces nothing, the finding is the surface's: give the trigger its
2759
+ `aria-expanded`, `aria-pressed`, or `aria-busy` rather than waiting on a framework's classes.
2760
+
2761
+ ### Wait for the paint to stop moving
2762
+
2763
+ A reading taken while paint is moving reports an interpolated frame no state of the interface
2764
+ paints. In the following fence `panel` transitions its `color` over 120ms.
2765
+
2766
+ ```ts
2767
+ import { readContrast, readStyle, waitForAnimations } from '@orkestrel/test/browser'
2768
+
2769
+ panel.classList.add('settle-done')
2770
+
2771
+ await waitForAnimations(panel, { budget: 2000 })
2772
+
2773
+ readStyle(panel, 'color') // 'rgb(0, 0, 0)'
2774
+ readContrast(panel) // measured against the settled paint rather than a frame in between
2775
+ ```
2776
+
2777
+ The wait parks on each animation's own `finished` promise and reads the list again after each
2778
+ completion, so an animation a finishing one starts is waited on too. An animation declaring infinite
2779
+ iterations is left out, which is what lets a page carrying a spinner settle at all. A detached
2780
+ element is refused rather than reported settled.
2781
+
2782
+ ### Read the refusal instead of catching it
2783
+
2784
+ Absent, present-but-gated, and ambiguous are different findings about an interface, so assert on the
2785
+ sentence rather than on a boolean. `readRefusal` drives the resolver and hands back its message, or
2786
+ nothing at all when the target resolves.
2787
+
2788
+ ```ts
2789
+ import { readRefusal } from '@orkestrel/test/browser'
2790
+
2791
+ readRefusal('Save changes') // undefined — the control resolves
2792
+ readRefusal('Menu') // 'Interactive target "Menu" is not visible and focus-reachable'
2793
+ readRefusal('Nowhere') // 'No interactive element has the accessible name "Nowhere"'
2794
+ readRefusal('Drafts') // 'Interactive target "Drafts" is ambiguous across 2 elements'
2795
+ readRefusal('tab', 'Drafts') // undefined — the role disambiguates it
2796
+ ```
2797
+
2798
+ Compare the whole sentence. A comparison against a fragment of one passes for a refusal about a
2799
+ different condition, which is the finding the distinct voices exist to keep apart.
2800
+
2364
2801
  ### Drive a field the component listens to
2365
2802
 
2366
2803
  Drive a field by name wherever the keystrokes are part of what the journey claims. Reach for these
@@ -2556,6 +2993,96 @@ The `extractStyles` reading is named for what it returns rather than `extractEsc
2556
2993
  `escape` term already carries the encoding sense in the `@orkestrel/html` and `@orkestrel/console`
2557
2994
  packages.
2558
2995
 
2996
+ ### Take an authored-class census
2997
+
2998
+ `readClasses` differenced against `readCascade` is the check every workspace writes; what each of
2999
+ them omits is the population it walked. `readCensus` reports both, and refuses a walk that read no
3000
+ element, because an empty walk reports the same empty difference as markup whose every class the
3001
+ cascade declares.
3002
+
3003
+ ```ts
3004
+ import { buildCensus, readCensus } from '@orkestrel/test/browser'
3005
+
3006
+ const control = buildCensus()
3007
+ screen.append(control.root)
3008
+
3009
+ const census = readCensus(screen)
3010
+ census.elements // 4 — the screen, the control's root, and the two marked elements
3011
+ census.undeclared // [control.mark, control.token] — sorted, the SVG one included
3012
+ ```
3013
+
3014
+ The control is the point of the fixture. One token rides on an HTML element and the other on an SVG
3015
+ element, whose `className` is an `SVGAnimatedString` rather than a string, so a census blind to the
3016
+ second reports one token where two are carried and reads as a clean screen.
3017
+
3018
+ ### Control a reading before you trust it
3019
+
3020
+ An instrument is not evidence until its control has failed. Each builder returns detached nodes for
3021
+ you to append where you are reading and remove afterwards, so nothing is mounted for you.
3022
+
3023
+ ```ts
3024
+ import {
3025
+ buildContrast,
3026
+ buildEscapes,
3027
+ extractStyles,
3028
+ mount,
3029
+ readContrast,
3030
+ } from '@orkestrel/test/browser'
3031
+
3032
+ const contrast = buildContrast(4.5)
3033
+ mount(contrast.root)
3034
+ readContrast(contrast.refused) < 4.5 // true
3035
+ readContrast(contrast.accepted) >= 4.5 // true
3036
+ contrast.root.remove()
3037
+
3038
+ const escapes = buildEscapes('project-stylesheet')
3039
+ extractStyles(escapes.root).length // 3
3040
+
3041
+ // Throws Error: Contrast control cannot straddle the bar 21
3042
+ buildContrast(21)
3043
+ ```
3044
+
3045
+ `buildContrast` is translucent over an opaque floor, so the compositing walk and the alpha blend
3046
+ `readContrast` exists for are the things under test: a reader taking the nearest declared background
3047
+ at full strength answers the opposite pair. The greys are searched rather than written down, so the
3048
+ stack follows the bar you asked for, and a bar no stack can straddle is refused rather than returned
3049
+ as a control that proves nothing.
3050
+
3051
+ `buildEscapes` carries the inline-attribute branch of a style-escape reading and the embedded-element
3052
+ branch, plus a `<style>` carrying the id a project exempts, so a reading that passes by refusing
3053
+ every `<style>` element fails that exemption instead of clearing it. Its root stays detached, which
3054
+ is what keeps an embedded sheet out of the cascade every other reading measures against.
3055
+
3056
+ ### Withhold a store the way a host does
3057
+
3058
+ A browser with site data blocked refuses every operation the permission withholds, and an origin
3059
+ with no room left refuses `setItem`. `createStorage` makes both reachable against a real `Storage`
3060
+ surface, backed by a map of its own: it patches neither browser surface and dispatches no `storage`
3061
+ event.
3062
+
3063
+ ```ts
3064
+ import { buildDenial, createStorage } from '@orkestrel/test/browser'
3065
+
3066
+ const storage = createStorage({ values: { theme: 'dark' }, reads: false, quota: 1 })
3067
+
3068
+ // Throws DOMException named SecurityError: Access is denied for getItem "theme"
3069
+ storage.getItem('theme')
3070
+
3071
+ storage.permit() // the grant a person allowing site data performs
3072
+ storage.getItem('theme') // 'dark'
3073
+
3074
+ storage.setItem('theme', 'light')
3075
+ // Throws DOMException named QuotaExceededError: No room is left for scale
3076
+ storage.setItem('scale', '2')
3077
+
3078
+ buildDenial('length').name // 'SecurityError'
3079
+ ```
3080
+
3081
+ `length`, `key`, and `getItem` are reads; `clear`, `removeItem`, and `setItem` are writes. `quota`
3082
+ counts accepted `setItem` calls rather than bytes, because the number of writes is what a journey
3083
+ scripts. `removeItem` consumes none of it and `permit` replenishes none of it: room and permission
3084
+ are different refusals, and a test that granted the permission still meets the full origin.
3085
+
2559
3086
  ### Remove an IndexedDB database
2560
3087
 
2561
3088
  Close the connections the test opened, then delete. A live connection blocks the deletion, and the
@@ -2752,7 +3279,12 @@ Each entry names the contracts its file proves. The test names carry the cases.
2752
3279
  exhaustion by attempts and by budget, producer throws counted as attempts with the last one kept as
2753
3280
  the cause, a predicate throw propagated unchanged, and an aborted retry. `waitForEvent` takes the
2754
3281
  exact delivered tuple, a timeout and an abort each naming the cleanup they invoked, and a second
2755
- delivery ignored after settlement. `decodeJSONLines` takes empty input, a trailing newline, CRLF,
3282
+ delivery ignored after settlement. `waitForText` takes a reading that arrives on a later poll, the
3283
+ containing reading its `exact` arm must refuse beside the whole reading that arm accepts, the frame
3284
+ carrying both the arrival and the departure that `absent` waits past, an empty expectation and an
3285
+ empty departure each refused, a reader throw propagated unchanged, the timeout naming the wait and
3286
+ the budget, an abort rejecting with the signal's own reason, and a refused bound raised through the
3287
+ family. `decodeJSONLines` takes empty input, a trailing newline, CRLF,
2756
3288
  line order, primitive lines, and a malformed physical line named with the native `SyntaxError` as
2757
3289
  its cause. `collect` and `collectStream` drain an empty and an ordered source, and the stream's
2758
3290
  reader lock is released afterwards. `roundTripJSON` takes a copy of a flat and a nested
@@ -2801,7 +3333,30 @@ Each entry names the contracts its file proves. The test names carry the cases.
2801
3333
  straddling an edge. `isReachable` takes a plain control and each condition it drops, a control the
2802
3334
  document no longer holds, a focusable SVG against an element from a foreign namespace, and the
2803
3335
  refused summary that proves it is the one filter the acting verbs apply; `isRendered` takes each
2804
- removal a browser honours and, as the split from `isReachable`, a zero-size announced control.
3336
+ removal a browser honours and, as the split from `isReachable`, a zero-size announced control. Each
3337
+ predicate also takes a subject inside an open and a closed shadow root beside a host that carries
3338
+ its own ancestor attribute — `[inert]` for one and `aria-hidden` for the other — and a host the flat
3339
+ tree does not lay out, which pins where the boundary falls for each.
3340
+ `pressKeys` takes a sequence reaching the control a traversal focused and, as the control, the same
3341
+ sequence refused while the document body holds focus with no keystroke recorded. `waitForState`
3342
+ takes a state a timer flips after the act, a node replaced mid-wait and still resolved by role and
3343
+ name, the reverse direction under `absent`, the exhaustion naming the control and the state and
3344
+ carrying the states last read, the resolver's own refusal propagated rather than spent as a
3345
+ reading, and a refused bound. `waitForAnimations` takes a descendant transition awaited to its end,
3346
+ the exhaustion naming the subject and the animation still running, an infinite-iteration animation
3347
+ excluded while it is still turning, a detached subject refused, and a refused bound; its settled
3348
+ reading is compared against the interpolated one taken mid-transition. `readRefusal` takes the
3349
+ absent, gated, and ambiguous voices beside a target that resolves under a bare name and under a
3350
+ role, and — as the control for the rethrow — a fixture element whose own `tabIndex` getter refuses
3351
+ with a string, handed straight back. `readCensus` takes a population carrying both undeclared
3352
+ tokens, tokens sorted rather than left in document order, a subtree whose every class the cascade
3353
+ declares, and an empty walk refused. `buildDenial` takes the keyed and unkeyed spellings of its
3354
+ `SecurityError`; `buildContrast` takes a composited stack straddling two different bars with the
3355
+ flat reading disagreeing for each foreground, and a bar refused at either end; `buildEscapes` takes
3356
+ both escapes and the exempt sheet reported, the exemption filtered by id, and a root that stays
3357
+ detached; `buildCensus` takes the SVG element whose class list is no string. The barrel takes every
3358
+ published name resolved from the specifier a consumer imports, and a bare `JourneyVariant` accepted
3359
+ wherever a `CaptureVariant` is asked for.
2805
3360
  `readHit` takes a centre that reaches the element itself, a reachable control under a cover that
2806
3361
  the reading names instead, a soft-wrapped inline target whose two line rectangles leave the box
2807
3362
  centre on its list item, and a control fixed outside the viewport, whose centre reaches nothing.
@@ -2910,7 +3465,11 @@ Each entry names the contracts its file proves. The test names carry the cases.
2910
3465
  an uncaught error and an unhandled rejection recorded and then ignored after the stop, the
2911
3466
  channels handed back by identity with a second stop proven a no-op against a replacement, a restart
2912
3467
  that clears `steps` and `output` without stacking wrappers, snapshots that stay what they were, and
2913
- one journal's recording kept out of another's.
3468
+ one journal's recording kept out of another's. `createStorage` takes a seeded store answering every
3469
+ operation, each withheld operation refused in its own voice, both permissions granted at once with
3470
+ the store answering from what it kept, a quota spent on accepted writes alone with `removeItem`
3471
+ consuming none of it, a granted permission replenishing no room, every refused quota value, and —
3472
+ as the control for the inertness claim — a write that leaves `localStorage` exactly as it was.
2914
3473
  - [`tests/src/server/helpers.test.ts`](../tests/src/server/helpers.test.ts) — the `readInventory` and
2915
3474
  wait-family contracts, and each pure leaf against its own inputs. `resolveContained` takes
2916
3475
  contained relative and absolute targets and both spellings of an escape, and `requireContained`
@@ -2982,8 +3541,9 @@ Each entry names the contracts its file proves. The test names carry the cases.
2982
3541
  boundary's uncallable-method and non-object-target refusals, the header flattening, the wait
2983
3542
  family's opposite throw directions with the exhaustion message and its `cause`, the statechart
2984
3543
  table walked against a real disclosure with the failing row's name opening the message and the
2985
- assertion kept as the `cause`, the cookie jar driven against a real origin, and the HTTP upgrade's
2986
- refused arm, claimed arm, and budget.
3544
+ assertion kept as the `cause`, the text wait resolving on the reading that carries the arrival
3545
+ without the departure and refusing an empty expectation, the cookie jar driven against a real
3546
+ origin, and the HTTP upgrade's refused arm, claimed arm, and budget.
2987
3547
 
2988
3548
  ## See also
2989
3549