@orkestrel/scaffold 0.0.72 → 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.
@@ -43,11 +43,13 @@ package holds one implementation of each and ships as a `devDependency`. Nothing
43
43
  production code. Source: [`src/core`](../src/core), [`src/browser`](../src/browser), and
44
44
  [`src/server`](../src/server).
45
45
 
46
- It has **zero runtime dependencies**, and no exported type here names an `@orkestrel/*` type. A
46
+ This package runtime-depends on `@orkestrel/contract` for the outcome type `retryUntil` reads
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
47
49
  dependency on `@orkestrel/emitter` would install a second copy of it beside the one a consumer
48
50
  already pins, and the compiler reads two copies as two distinct types. A foreign type in a
49
51
  signature fails the other way, rejecting the consumer's own local value inside the consumer's own
50
- repository. The zero-runtime-dependencies contract holds both.
52
+ repository. Those rules hold both.
51
53
 
52
54
  ## Install
53
55
 
@@ -110,31 +112,30 @@ member and `plus` introducing its call-signature members, and a type alias's own
110
112
  a union's arms escaped as `\|`. An extended interface's name comes before `plus`, with the members
111
113
  it adds after.
112
114
 
113
- | Type | Kind | Shape | Summary |
114
- | -------------------------- | --------- | ------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
115
- | `WaitOptions` | interface | `{ budget?, interval?, signal? }` | Configures a bounded asynchronous wait with an elapsed-time limit, a delay between readings, and an abort signal. |
116
- | `RetryOptions` | interface | `WaitOptions` plus `{ attempts? }` | Configures a bounded retry, adding an optional producer-call limit to a bounded wait's bounds. |
117
- | `EventSubscriber` | type | `(listener) => cleanup \| void` | Subscribes a listener to one event source. |
118
- | `RecorderInterface` | interface | `{ calls, count, handler }` plus `clear` | Records every call made to its handler. |
119
- | `EventSourceInterface` | interface | `{} plus on` | Subscribes handlers to a typed event source. |
120
- | `RecorderMap` | type | `{ readonly [K in TName]: RecorderInterface<TMap[K]> }` | Maps event names to recorders for their delivered argument tuples. |
121
- | `Success` | interface | `{ success, value }` | Represents one operation that produced a value. |
122
- | `Failure` | interface | `{ success, error }` | Represents one operation that raised a failure instead of producing a value. |
123
- | `Result` | type | `Success<T> \| Failure<E>` | Represents the outcome of one operation: the value it produced, or the failure it raised. |
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
- | `JSONValue` | type | `string \| number \| boolean \| null \| readonly JSONValue[] \| { readonly [key: string]: JSONValue }` | Covers any value JSON can represent, so a round trip through JSON preserves the type. |
130
- | `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`. |
131
- | `HeadersSource` | type | `NonNullable<ConstructorParameters<typeof Headers>[0]>` | Covers any value the host `Headers` constructor accepts. |
132
- | `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. |
133
- | `StateScenario` | interface | `{ transition }` plus `arrange` / `act` / `assert` | Drives one `StateTransition` through the three phases that prove it. |
134
-
135
- Each interface's call-signature members are listed under [Methods](#methods). `Result` defaults `E`
136
- to `Error`, where `@orkestrel/contract` publishes the same name defaulting to `unknown`;
137
- [Limits](#limits) rules that divergence.
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. |
135
+
136
+ Each interface's call-signature members are listed under [Methods](#methods). `Result`, `Success`,
137
+ and `Failure` come from `@orkestrel/contract` (mirrored at [`contract.md`](contract.md)) and are not
138
+ re-exported; `retryUntil` reads `Result` internally.
138
139
 
139
140
  #### Constants
140
141
 
@@ -148,8 +149,13 @@ A `Shape` cell holds the constant's declared type.
148
149
  A harness renders the attributes and a gate outside the page polls them, so the names are the whole
149
150
  contract between the two. `status`, `passed`, `failed`, and `total` belong on the harness root,
150
151
  `scenario` and `result` on each row, and `state` on the element rendering the entity's current
151
- state. `pending` is what a harness carries before a run has produced a result for every row, and
152
- `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.
153
159
 
154
160
  #### Validators
155
161
 
@@ -176,8 +182,10 @@ instead of propagating.
176
182
  | `waitForCondition` | function | `(description, condition, options?) => Promise<void>` | Waits until a condition holds within an elapsed-time budget. |
177
183
  | `retryUntil` | function | `(description, produce, satisfied, options?) => Promise<T>` | Repeats a producer until one produced value satisfies a predicate. |
178
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. |
179
186
  | `checkBounds` | function | `(subject: string, budget: number, interval: number) => void` | Checks the resolved bounds one bounded wait runs under. |
180
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. |
181
189
  | `dropRegistration` | function | `(registrations: SignalRegistration[], installed) => SignalRegistration \| undefined` | Drops the registration an instrumented signal installed for one listener. |
182
190
  | `decodeJSONLines` | function | `(text: string) => readonly unknown[]` | Decodes newline-delimited JSON values. |
183
191
  | `waitForDelay` | function | `(ms?: number) => Promise<void>` | Waits for a host timer to elapse. |
@@ -219,17 +227,21 @@ a journey a description of what a person does rather than of what the markup hap
219
227
 
220
228
  The fixture builders, the readers, and the field writers do take an element, and none of them is a
221
229
  journey verb. `build` creates a node, `mount` attaches one, and `render` does both from
222
- markup or from a tag and its classes; `clearStorage` takes nothing at all, and `removeDatabase`
223
- takes a database name. The predicates, the element readers, and the describers name a node the
224
- caller already has — `isRendered`, `isReachable`, `readText`, `readRole`, `readName`, `readStates`,
225
- `describeTree`, `describeFocus`, `extractOrphans`, `readRows`, `readStyle`, `readToken`,
226
- `readPixels`, `readContrast`, `readLayers`, `readBackdrop`, and `readRing` — and each reads that
227
- node rather than acting on a target it was handed. `captureFrame` and `place` take an element as
228
- well, and photographing one is a reading too: neither moves focus, dispatches an event, or changes
229
- what the element renders. `typeInput` and `commitInput` are the exception, and it stays narrow: they
230
- write into the field they are given, as the synthetic counterpart of `typeAccessible` for a
231
- component that listens for `input`. The color leaves, the cascade readers, the pane verbs, and the
232
- whole-document readers take a value or nothing at all, so they name no target either.
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
240
+ element as well, and photographing one is a reading too: neither moves focus, dispatches an event,
241
+ or changes what the element renders. `typeInput` and `commitInput` are the exception, and it stays
242
+ narrow: they write into the field they are given, as the synthetic counterpart of `typeAccessible`
243
+ for a component that listens for `input`. The color leaves, the cascade readers, the pane verbs,
244
+ and the whole-document readers take a value or nothing at all, so they name no target either.
233
245
 
234
246
  #### Types
235
247
 
@@ -237,17 +249,26 @@ A `Shape` cell holds an interface's data members as bare names in braces, `?` ma
237
249
  member and `plus` introducing its call-signature members, and a type alias's own type literal with
238
250
  a union's arms escaped as `\|`.
239
251
 
240
- | Type | Kind | Shape | Summary |
241
- | -------------------- | --------- | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
242
- | `Color` | type | `readonly [red, green, blue, alpha]` | Represents one rendered color as straight sRGB channels and its alpha. |
243
- | `ElementOptions` | interface | `{ classes?, text?, attributes? }` | Configures one built element: its class list, its text, and its attributes. |
244
- | `FrameOptions` | interface | `{ path, width, height, element? }` | Configures one captured frame: where it is written, the viewport it is shot at, and what it shoots. |
245
- | `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. |
246
- | `CaptureVariant` | interface | `{ name, width, height, apply? }` | Represents one theme-and-viewport pair a capture run renders, and the document change it needs first. |
247
- | `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. |
248
- | `PortfolioInterface` | interface | `{ variant, placements, paths, files }` plus `place` | Holds the registry of capture states one run places, and the files it wrote placing them. |
249
- | `JournalStep` | interface | `{ action, trigger, result }` | Represents one scripted step a journal recorded, and what the surface did about it. |
250
- | `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. |
251
272
 
252
273
  #### Constants
253
274
 
@@ -267,66 +288,76 @@ A `Shape` cell holds the constant's declared type.
267
288
 
268
289
  #### Helpers
269
290
 
270
- | API | Kind | Signature | Summary |
271
- | ----------------------- | -------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
272
- | `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. |
273
- | `resolveRendered` | function | `(first: string, second?: string) => HTMLElement` | Resolves one rendered, focus-reachable interactive element without requiring it to intersect the viewport yet. |
274
- | `computeNamePattern` | function | `(name: string) => RegExp` | Computes the pattern that matches one accessible name a decorative glyph may sit beside. |
275
- | `isOutsideViewport` | function | `(rectangle: DOMRectReadOnly) => boolean` | Determines whether a rectangle lies wholly outside the browser viewport. |
276
- | `isRendered` | function | `(element: Element) => boolean` | Determines whether the accessibility tree presents one element at all. |
277
- | `isReachable` | function | `(element: Element) => boolean` | Determines whether a person can click one element where it sits. |
278
- | `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. |
279
- | `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. |
280
- | `clickDisclosure` | function | `(name: string) => Promise<void>` | Opens or closes one native details disclosure by its rendered summary. |
281
- | `typeAccessible` | function | `(name: string, text: string) => Promise<void>` | Replaces a named field's value through focus, select-all, deletion, and real keystrokes. |
282
- | `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. |
283
- | `traverseAccessible` | function | `(name: string) => Promise<HTMLElement>` | Reaches a named control only through natural forward Tab traversal from the current focus. |
284
- | `readPerception` | function | `(name: string) => string` | Reads the normalized visible text of one named region, dialog, table, tab panel, or alert. |
285
- | `readPage` | function | `() => string` | Reads the normalized visible text of the whole page. |
286
- | `readFocus` | function | `() => string \| undefined` | Reads the rendered text of the element that holds focus. |
287
- | `readValue` | function | `(role: string, name: string) => string` | Reads the value a resolved control renders. |
288
- | `readText` | function | `(element: Element) => string` | Reads one element's rendered text the way a name computation reads it. |
289
- | `readRole` | function | `(element: Element) => string \| undefined` | Reads the role one element carries in the accessibility tree. |
290
- | `readName` | function | `(element: Element) => string` | Reads the accessible name one element is announced under. |
291
- | `readStates` | function | `(element: Element) => readonly string[]` | Reads the states one element is announced in. |
292
- | `describeTree` | function | `(element: Element) => string` | Describes the accessible tree one rendered element presents. |
293
- | `describeFocus` | function | `(element: Element) => string` | Describes the order sequential keyboard navigation visits one element's controls in. |
294
- | `waitForFrame` | function | `() => Promise<void>` | Waits for one animation frame to settle pending browser paint work. |
295
- | `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. |
296
- | `mount` | function | `<T extends Element>(element: T) => T` | Puts one element into the document and hands it straight back. |
297
- | `render` | function | `(markup: string) => HTMLDivElement` / `(tag: K, classes: string) => HTMLElementTagNameMap[K]` | Renders one fixture into the document from trusted markup. |
298
- | `typeInput` | function | `(element: HTMLInputElement \| HTMLTextAreaElement, text: string) => void` | Sets one field's value and announces it the way typing into the field does. |
299
- | `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. |
300
- | `clearStorage` | function | `() => void` | Clears both browser storage surfaces. |
301
- | `removeDatabase` | function | `(name: string) => Promise<void>` | Deletes one IndexedDB database and reports what the request actually did. |
302
- | `parseColor` | function | `(value: string) => Color \| undefined` | Parses one computed CSS color value into straight sRGB channels. |
303
- | `parseCSSColor` | function | `(value: string) => Color \| undefined` | Resolves any CSS color expression to straight sRGB channels, by asking the browser. |
304
- | `matchesColor` | function | `(first: string \| Color, second: string \| Color) => boolean` | Determines whether two colors render the same, within the rounding a browser does. |
305
- | `blendColor` | function | `(front: Color, back: Color) => Color` | Composites one color over another. |
306
- | `measureLuminance` | function | `(color: Color) => number` | Measures one opaque color's WCAG relative luminance. |
307
- | `measureContrast` | function | `(front: Color, back: Color) => number` | Measures the WCAG 2.x contrast ratio between two opaque colors. |
308
- | `readLayers` | function | `(element: Element) => readonly Color[]` | Collects the painted layers standing between one element and the surface it sits on. |
309
- | `readBackdrop` | function | `(element: Element, floor: Color) => Color` | Resolves the opaque color standing behind one element. |
310
- | `readContrast` | function | `(element: Element, floor?: Color) => number` | Measures the WCAG 2.x contrast ratio between an element's computed text and background colors. |
311
- | `readRing` | function | `(control: Element, worn?: Element) => number \| undefined` | Measures the contrast the focus chrome painted on one control reaches against its own backdrop. |
312
- | `measureContent` | function | `() => number` | Measures the row the document's own content ends on, in document coordinates. |
313
- | `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. |
314
- | `releasePane` | function | `() => Promise<void>` | Hands the tester pane back to the runner's own layout, at the viewport it had before staging. |
315
- | `captureFrame` | function | `(options: FrameOptions) => Promise<string>` | Shoots one frame at one viewport size and proves the file on disk holds this run's bytes. |
316
- | `readFrame` | function | `(path: string) => Promise<FrameReading>` | Reads one written frame back and reports its size and the color its bottom row paints. |
317
- | `readCascade` | function | `() => ReadonlySet<string>` | Collects every class token the stylesheets loaded into this document actually define. |
318
- | `readClasses` | function | `(root: ParentNode) => ReadonlySet<string>` | Collects every class token the markup under one root carries. |
319
- | `readRules` | function | `() => readonly CSSRule[]` | Collects every rule the stylesheets loaded into this document hold, nested grouping rules included. |
320
- | `findRule` | function | `(selector: string) => CSSStyleRule \| undefined` | Finds the first style rule in the cascade whose selector carries a fragment. |
321
- | `findKeyframes` | function | `(name: string) => CSSKeyframesRule \| undefined` | Finds the animation the cascade declares under one name. |
322
- | `readRows` | function | `(root: ParentNode, selector: string) => readonly string[]` | Reads the normalized visible text of every element a selector matches, in document order. |
323
- | `extractOrphans` | function | `(root: ParentNode, child: string, parent: string) => readonly string[]` | Collects every element carrying a component class rendered outside the container it belongs to. |
324
- | `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`. |
325
- | `readStyle` | function | `(element: Element, property: string) => string` | Reads one resolved CSS property from a real browser element. |
326
- | `readToken` | function | `(element: Element, name: string) => string` | Reads one custom property from an element's resolved style. |
327
- | `readRootToken` | function | `(name: string) => string` | Reads one custom property from the document element. |
328
- | `readPixels` | function | `(element: Element, property: string) => number` | Reads one resolved CSS length as a number of pixels. |
329
- | `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. |
330
361
 
331
362
  #### Factories
332
363
 
@@ -337,6 +368,8 @@ A `Shape` cell holds the constant's declared type.
337
368
  | `createPortfolio` | function | `(options: PortfolioOptions) => PortfolioInterface` | Creates the capture portfolio one run places its screenshots through. |
338
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. |
339
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. |
340
373
 
341
374
  `resolveAccessible` counts a match as reachable only when every condition holds: it is connected; it
342
375
  passes a visibility check honouring opacity and CSS; its box has non-zero width and height; its
@@ -836,6 +869,36 @@ earlier [Surface](#surface) rows.
836
869
  | `stop` | `void` | Stops recording and hands every intercepted console channel back by identity. |
837
870
  | `record` | `void` | Records one step, when the journal is started. |
838
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
+
839
902
  #### `ScratchInterface`
840
903
 
841
904
  | Method | Returns | Summary |
@@ -928,43 +991,71 @@ what the link reaches, not on what it stores.
928
991
  Every message `src/browser` throws. Keep them distinct: a journey asserts the one it means, and
929
992
  absent, present-but-gated, and ambiguous are different findings about an interface.
930
993
 
931
- | Voice | Thrown by |
932
- | ------------------------------------------------------------------------------------- | ----------------------- |
933
- | `No interactive element has the accessible name "<name>"` | `resolveRendered` |
934
- | `Interactive target "<name>" is not visible and focus-reachable` | `resolveRendered` |
935
- | `Interactive target "<name>" is ambiguous across <n> elements` | `resolveRendered` |
936
- | `Interactive target "<name>" could not be resolved` | `resolveRendered` |
937
- | `Interactive target "<name>" is unreachable after scrolling` | `resolveAccessible` |
938
- | `Interactive target "<name>" is not reachable inside "<region>"` | `clickAccessibleWithin` |
939
- | `Interactive target "<name>" is ambiguous across <n> elements inside "<region>"` | `clickAccessibleWithin` |
940
- | `Interactive target "<name>" could not be resolved inside "<region>"` | `clickAccessibleWithin` |
941
- | `Native disclosure "<name>" is not visible and focus-reachable` | `clickDisclosure` |
942
- | `Native disclosure "<name>" is ambiguous across <n> elements` | `clickDisclosure` |
943
- | `Native disclosure "<name>" could not be resolved` | `clickDisclosure` |
944
- | `Interactive target "<name>" is not reachable through forward Tab traversal: <trail>` | `traverseAccessible` |
945
- | `Named region "<name>" is not visible` | `readPerception` |
946
- | `Named region "<name>" is ambiguous across <n> elements` | `readPerception` |
947
- | `Named region "<name>" could not be resolved` | `readPerception` |
948
- | `Interactive target "<name>" does not carry a value` | `readValue` |
949
- | `Computed foreground color is unavailable` | `readContrast` |
950
- | `Computed background color is unavailable` | `readContrast` |
951
- | `Tester pane is unavailable for a capture` | `stagePane` |
952
- | `Tester pane rendered <w>x<h> for a <w>x<h> viewport` | `stagePane` |
953
- | `Capture frame at <path> never settled after <n> restagings: <h> over a <h> pane` | `captureFrame` |
954
- | `Capture frame was written to <path> where <path> was asked for` | `captureFrame` |
955
- | `Capture frame at <path> is not the one this run shot` | `captureFrame` |
956
- | `Capture frame at <path> could not be read` | `readFrame` |
957
- | `Capture frame at <path> is not an image this browser decodes` | `readFrame` |
958
- | `Capture frame at <path> cannot be measured without a 2D canvas` | `readFrame` |
959
- | `Capture variant "<name>" is not registered` | `createPortfolio` |
960
- | `Capture state "<state>" is not registered` | `place` |
961
- | `Capture state "<state>" is already placed` | `place` |
962
- | `IndexedDB database "<name>" could not be deleted` | `removeDatabase` |
963
- | `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.
964
1052
 
965
1053
  Some of them are narrowing rather than findings, and no input reaches them. Each `could not be
966
1054
  resolved` is one: a preceding length check does not narrow the later lookup under
967
- `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.
968
1059
 
969
1060
  The capture guards are the other population no test drives, because each answers for a runner or a
970
1061
  provider this package does not control. `Tester pane is unavailable for a capture` fires where
@@ -1142,9 +1233,17 @@ These hold across `src/core`, `src/browser`, `src/server`, and this guide.
1142
1233
  fixture looking alive. `remove` is written out for the opposite reason: `rmSync` with `force`
1143
1234
  does not throw on a path that is not there, so without the check it would report success against
1144
1235
  a fixture that is gone.
1145
- 9. **Zero runtime dependencies, and no foreign type in a signature.** `dependencies` is empty and
1146
- stays empty. No exported signature names an `@orkestrel/*` type, so no consumer can be handed a
1147
- 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.
1148
1247
  10. **`createTeardown` runs newest-first, and every handler runs.** `destroy()` takes the registered
1149
1248
  handlers in reverse registration order and awaits each one before starting the next, so a
1150
1249
  handler that undoes what a later registration depends on runs after it. A handler that throws or
@@ -1156,9 +1255,10 @@ These hold across `src/core`, `src/browser`, `src/server`, and this guide.
1156
1255
  joining this one, and `count` read from inside a running handler counts only those late
1157
1256
  registrations. A repeated `destroy()` runs nothing that already ran, which is what makes it
1158
1257
  idempotent. The list registers no Vitest hook itself: the consumer writes
1159
- `afterEach(() => teardown.destroy())` once, in its own setup. That one line is the price of the
1160
- zero-dependency contract, because registering the hook here would take a runtime dependency on
1161
- 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.
1162
1262
  11. **`createLoopback` binds a server the caller made.** The caller constructs its own unstarted
1163
1263
  server and keeps every protocol handler on it; this package supplies the bind and the release
1164
1264
  and nothing else. It listens on port `0` at `127.0.0.1`, so the host assigns the port and the
@@ -1188,24 +1288,29 @@ These hold across `src/core`, `src/browser`, `src/server`, and this guide.
1188
1288
  into a description of the markup. `build` creates a node, `mount` attaches one, `render` does
1189
1289
  both, `clearStorage` takes nothing at all, and `removeDatabase` takes a database name. The
1190
1290
  predicates, the element readers, and the describers do take a node —
1191
- `isRendered`, `isReachable`, `readText`, `readRole`, `readName`, `readStates`, `describeTree`,
1192
- `describeFocus`, `extractOrphans`, `readRows`, `readStyle`, `readToken`, `readPixels`,
1193
- `readContrast`, `readLayers`, `readBackdrop`, and `readRing` — and each is a reader of a node
1194
- the caller already has rather than a verb that acts on a target. `captureFrame` and `place` take
1195
- one as the subject of a photograph, which is a reading too: neither moves focus, dispatches an
1196
- event, nor changes what the element renders. `typeInput` and `commitInput` are the one pair that
1197
- acts on the element it is handed, and the exception is deliberately narrow: they are the
1198
- synthetic counterpart of `typeAccessible`, for a component that listens for `input` and a test
1199
- that already holds the field. Drive the field by name wherever the keystrokes are part of what
1200
- the journey claims. `readRing` is the case that makes the split explicit. It measures the focus
1201
- chrome a browser painted and never brings the focus about, so a journey reaches the control
1202
- through `traverseAccessible` or `userEvent.keyboard` from `vitest/browser` and then measures what
1203
- landed. The whole environment imports `vitest/browser` and DOM globals and nothing else — no
1204
- `src/core` import, no framework, no `node:*`, and no `import.meta.env`, so whether a run writes
1205
- captures is the consumer's decision through `PortfolioOptions.enabled` rather than an environment
1206
- variable this package reads. `vitest` is a peer dependency, so the provider the layer drives is
1207
- the one the consumer already installed, and the zero-runtime-dependencies contract's empty
1208
- `dependencies` is untouched.
1291
+ `isRendered`, `isReachable`, `readHit`, `readText`, `readRole`, `readName`, `readStates`,
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`
1298
+ and `place` take one as the subject of a photograph, which is a reading too: neither moves
1299
+ focus, dispatches an event, nor changes what the element renders. `typeInput` and `commitInput`
1300
+ are the one pair that acts on the element it is handed, and the exception is deliberately
1301
+ narrow: they are the synthetic counterpart of `typeAccessible`, for a component that listens for
1302
+ `input` and a test that already holds the field. Drive the field by name wherever the keystrokes
1303
+ are part of what the journey claims. `readRing` is the case that makes the split explicit. It
1304
+ measures the focus chrome a browser painted and never brings the focus about, so a journey
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.
1209
1314
  14. **The wait family polls only where nothing publishes an event.** The no-polling architecture law
1210
1315
  governs a product's idle wakeup: a running system parks on the event or the abort signal that
1211
1316
  fires. A test instrument is the other case. It waits on a fact another process produces — a file
@@ -1214,7 +1319,15 @@ These hold across `src/core`, `src/browser`, `src/server`, and this guide.
1214
1319
  budget measured with `performance.now()`. Where an event does exist, `waitForEvent` is the door:
1215
1320
  it parks on the subscription, validates the interval for consistency with the family and never
1216
1321
  uses it, and invokes the cleanup the subscriber returned on timeout, on abort, and on delivery
1217
- 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,
1218
1331
  and that description is what the timeout message carries — a wait nobody described times out
1219
1332
  saying nothing about what failed. Every bound is validated finite and non-negative before
1220
1333
  anything is read, a budget of `0` still permits the immediate first reading, and an abort rejects
@@ -1250,6 +1363,13 @@ These hold across `src/core`, `src/browser`, `src/server`, and this guide.
1250
1363
  narrow their own candidates and then keep the ones it accepts — so a journey meets one rule
1251
1364
  rather than near-copies of it. Neither asks about the viewport; `resolveAccessible` scrolls a
1252
1365
  wholly off-viewport target into view and measures that separately with `isOutsideViewport`.
1366
+ `readHit` reads beside that pair rather than filtering with it. It hit-tests one point — the
1367
+ element's own bounding-box centre — which is how it sees what neither predicate can: a cover
1368
+ over a control they both accept, and a wrapped inline target whose centre falls between its line
1369
+ boxes. No acting verb consults it, because it names a node rather than ruling, and `isReachable`
1370
+ stays the one reachability filter the verbs apply. It is also the reader that needs the pair run
1371
+ first, and [Bounds a shipped helper carries](#bounds-a-shipped-helper-carries) states what it
1372
+ reports for an element that failed them.
1253
1373
  17. **A journal forwards every console call and swallows nothing.** A browser publishes no listener
1254
1374
  for its own output, so `createJournal` stands in front of the console and hands each call on to
1255
1375
  the channel that was there when `start` armed it. A run under a journal therefore prints exactly
@@ -1285,6 +1405,25 @@ These hold across `src/core`, `src/browser`, `src/server`, and this guide.
1285
1405
  `releasePane` returns the tester to the viewport it held before the staging, so the variant a
1286
1406
  frame was shot at belongs to that frame alone, and a suite that wants a size of its own calls
1287
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.
1288
1427
 
1289
1428
  ### Threat model
1290
1429
 
@@ -1373,14 +1512,29 @@ or when a consumer appears the ruling did not consider.
1373
1512
  | A DOM element builder | Ships | It ships as `build` for the element and `mount` for the attachment, and `render` widened to take a tag and its class list as well as markup. A class list, a text, and an attribute map are what a fixture actually varies, and expressing that variation through markup means assembling a string. Nothing here assembles a tree one call at a time: a fixture with children is still written as markup. |
1374
1513
  | A surface digest — `describeSurface` | Refused | Its digest format is one workspace's policy about what a summary of a surface contains, and it is assembled from the excluded `extractControls` besides. `describeTree` and `describeFocus` publish the readings a digest is built from instead. |
1375
1514
  | A control extractor — `extractControls` | Refused | Generalized past its one caller it is a wrapper over `querySelectorAll` that adds no boundary, invariant, composition, or narrower contract, which is what the superfluous-wrapper rule refuses. |
1515
+ | A pointer-centre hit reading — `readHit` | Ships | It ships as `readHit`. `extractControls` is the bar it has to clear, and it does: the centre computation composes a rectangle reading with a hit test, the `undefined` translation is this package's absence convention for a reader, and "the point is always this element's centre" is a materially narrower contract than `elementFromPoint`. `roughnotes` writes that composition inline in `App.test.ts`, `integration.test.ts`, and the `ContactForm`, `PaymentForm`, and `SubscribeForm` suites, each against the cover and the wrapped target `isReachable` cannot see. |
1376
1516
  | Text resolution by selector — `resolveText` | Refused | The journey-layer contract is the one it breaks: a journey verb resolves its own target from a role and an accessible name, and one that takes a selector turns a journey into a description of the markup. Taking a node the test already holds is a different thing, which is what the element readers do; `findRule` takes a selector because its subject is the stylesheet rather than a target to act on. |
1377
1517
  | A hand-driven timer — `terminal`, `toolbox` | Refused | `toolbox` runtime-depends on `terminal`, so the two are one implementation rather than independent demand. The shape is also `@orkestrel/terminal`'s published `TimerHandler`, which a copy here would redeclare unversioned and hand consumers a second incompatible type. |
1378
1518
  | A hand-driven clock — `mcp`, `middleware` | Refused | `AGENTS.md` bans replacing the host clock outright, so publishing one from the fleet's own test package would sanction across every workspace the substitution those rules refuse. `waitForDelay` waits on a real host timer and `waitForCondition` bounds a real elapsed interval with `performance.now()`. |
1379
1519
  | A reserve-then-release port picker | Refused | It binds a port, closes it, and hands the number to a child that binds it again, and the window between that close and that rebind is a race another process on the host can win. Have the child bind `0` and report back the port it was given; `createLoopback` does exactly that for a server the test owns itself. |
1380
1520
  | An abort-signal wait — `waitForAbort` | Ships | It ships as `waitForAbort`. Every bounded member still takes `WaitOptions.signal` and rejects with the signal's own reason, so a bounded wait needs nothing here; this answers the other case, where the abort is itself the fact the test waits for. It parks on a one-shot listener with no timer and no budget, so a signal that never aborts is the caller's own deadlock rather than a timeout this could name. |
1381
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. |
1382
- | An outcome triple — a produced arm, a failed arm, and their union | Ships | It ships as `Success`, `Failure`, and `Result`. The rule governing it permits a local declaration only where no declared dependency already carries one, and this package declares no runtime dependency at all, so it cannot import `@orkestrel/contract`'s. That buys a divergence a consumer holding both packages meets: the two `Success<T>` declarations carry identical members and so do the two `Failure<E>` declarations, while `@orkestrel/test`'s `Result<T, E = Error>` defaults its failure type to `Error` and `@orkestrel/contract`'s `Result<T, E = unknown>` leaves it `unknown`. No signature published here returns one — `retryUntil` reads the type internally — so a workspace holding both packages takes its outcome from `@orkestrel/contract` and reaches for this one only where the value came from this package. |
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. |
1383
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. |
1384
1538
 
1385
1539
  `ScratchInterface`'s own members were ruled the same way, and coherence rather than demand decided
1386
1540
  them. `ensure` ships because it is the one member that produces an empty directory — `write` always
@@ -1422,6 +1576,12 @@ the helper rather than to the host, and each names what to reach for instead.
1422
1576
  key reads `undefined` at runtime under a non-optional type and `isRecorderMapComplete` still reports
1423
1577
  `true`, because it checks the events it was given rather than the type it was keyed by. Pass a
1424
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.
1425
1585
  - **`readProperty`'s `TypeError` names the target, never the read.** It refuses a target that is
1426
1586
  neither an object nor a function before it reads anything, and a getter that throws on an accepted
1427
1587
  target hands that throw straight to the caller. Wrap the call in `captureError` where a hostile
@@ -1430,6 +1590,32 @@ the helper rather than to the host, and each names what to reach for instead.
1430
1590
  carrying no leading number — `'auto'`, `'none'`, `''` — reads as `0`, because none of them
1431
1591
  contributes a pixel to what a reader sees, so a caller cannot tell an unparsable value from a
1432
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.
1606
+ - **`readHit` answers for one point, and a node it returns is no proof of a cover.** An element the
1607
+ document does not render measures a zero rectangle at the origin, and a zero-area element measures
1608
+ a point on its own edge, so each is hit-tested like any other point and names whatever paints
1609
+ there — the surrounding container, or the document body for a rectangle collapsed at the origin —
1610
+ while `contains` reads false. A cover painted with `pointer-events: none` is absent from the hit
1611
+ test, so the reading names the element underneath it and the caller reads reachable for a cover a
1612
+ person can see. An element inside a shadow tree retargets in an open root and a closed one alike:
1613
+ the document-level hit test names the host, which the inner element does not contain. Run
1614
+ `isRendered` and `isReachable` first, and ask `element.getRootNode()` for its own
1615
+ `elementFromPoint` where the subject sits in a shadow tree. `undefined` carries the other silence:
1616
+ a centre outside the viewport reads the same as a centre that reaches nothing, and
1617
+ `isOutsideViewport` does not separate them, because it asks whether the whole rectangle misses the
1618
+ viewport while this asks where one point lands.
1433
1619
 
1434
1620
  ## Patterns
1435
1621
 
@@ -1755,6 +1941,39 @@ Every bounded member takes an `AbortSignal` and rejects with the signal's own re
1755
1941
  controller ends a whole file's waits. A budget of `0` still permits the immediate first reading, and
1756
1942
  a bound that is not finite and non-negative is refused before anything is read.
1757
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
+
1758
1977
  ### Copy a JSON value
1759
1978
 
1760
1979
  This demonstration builds an interface-typed value, copies it through JSON serialization, and
@@ -1855,47 +2074,133 @@ expect(JSON.stringify(serializeSchema(received))).toBe(wire)
1855
2074
  A statechart table is a row per transition, and a row is the transition plus the three phases that
1856
2075
  prove it: `arrange` puts the entity into `from`, `act` applies the `event`, and `assert` reads the
1857
2076
  entity for `to`. `executeScenarios` walks the table and hands each row a context of its own; it
1858
- registers nothing, so `describe` and `it` stay where you write them. In the following fence,
1859
- `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.
1860
2090
 
1861
2091
  ```ts
1862
2092
  import type { StateScenario } from '@orkestrel/test'
1863
- import { executeScenarios } from '@orkestrel/test'
2093
+ import { executeScenarios, requireValue } from '@orkestrel/test'
2094
+ import { clickAccessible, clickDisclosure, readStates, render } from '@orkestrel/test/browser'
1864
2095
  import { expect, it } from 'vitest'
1865
2096
 
1866
2097
  type DisclosureState = 'closed' | 'open'
1867
- type DisclosureEvent = 'show' | 'hide'
2098
+ type DisclosureEvent = 'toggle' | 'dismiss'
1868
2099
 
1869
2100
  interface DisclosureContext {
1870
- 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)
1871
2141
  }
1872
2142
 
1873
2143
  const SCENARIOS: ReadonlyArray<StateScenario<DisclosureState, DisclosureEvent, DisclosureContext>> =
1874
2144
  [
1875
2145
  {
1876
- transition: { name: 'closed opens on show', from: 'closed', event: 'show', to: 'open' },
1877
- arrange(context, state) {
1878
- if (state === 'open') context.disclosure.show()
2146
+ transition: {
2147
+ name: 'closed opens through the summary',
2148
+ from: 'closed',
2149
+ event: 'toggle',
2150
+ to: 'open',
1879
2151
  },
1880
- act(context, event) {
1881
- if (event === 'show') context.disclosure.show()
1882
- 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',
1883
2162
  },
1884
- assert(context, state) {
1885
- 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',
1886
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,
1887
2189
  },
1888
- // One row per transition. Each row reuses the three phases shown earlier.
1889
2190
  ]
1890
2191
 
1891
2192
  it('walks the disclosure statechart', async () => {
1892
- await executeScenarios(SCENARIOS, () => ({ disclosure: new Disclosure() }))
2193
+ await executeScenarios(SCENARIOS, buildDisclosure)
1893
2194
  })
1894
2195
  ```
1895
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
+
1896
2202
  Both unions are the entity's own vocabulary, so a row naming a state or an event the entity does not
1897
- have fails to typecheck rather than at runtime. Each phase reads its subject from its own parameters
1898
- 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.
1899
2204
 
1900
2205
  The rows run one after another, because a statechart's rows drive one entity and a parallel run
1901
2206
  would have them arranging over each other. The run stops at the first row that fails, and the row's
@@ -1908,18 +2213,27 @@ const MISMATCHED: ReadonlyArray<
1908
2213
  StateScenario<DisclosureState, DisclosureEvent, DisclosureContext>
1909
2214
  > = [
1910
2215
  {
1911
- transition: { name: 'show leaves it closed', from: 'closed', event: 'show', to: 'closed' },
1912
- // 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,
1913
2227
  },
1914
2228
  ]
1915
2229
 
1916
- await executeScenarios(MISMATCHED, () => ({ disclosure: new Disclosure() }))
1917
- // 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'
1918
2232
 
1919
2233
  await executeScenarios(MISMATCHED, () => {
1920
2234
  throw new Error('no fixture')
1921
2235
  })
1922
- // Error: show leaves it closed: build refused
2236
+ // Error: the summary leaves it closed: build refused
1923
2237
  ```
1924
2238
 
1925
2239
  Whatever the phase threw arrives as that error's `cause`, by identity, so an assertion's own detail
@@ -1928,10 +2242,61 @@ survives the renaming. A phase that throws something other than an `Error` is na
1928
2242
  builder's refusal arrives as the `cause` the same way, and the phases of the row it was building for
1929
2243
  never start.
1930
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
+
1931
2255
  Drive one row on its own with `executeScenario`, which takes the context rather than building it.
1932
2256
 
1933
- A harness that renders the same table in a browser publishes its progress through attributes, and
1934
- `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.
1935
2300
 
1936
2301
  ```ts
1937
2302
  import { STATECHART_ATTRIBUTES, STATECHART_STATUSES } from '@orkestrel/test'
@@ -1939,15 +2304,23 @@ import { STATECHART_ATTRIBUTES, STATECHART_STATUSES } from '@orkestrel/test'
1939
2304
  STATECHART_ATTRIBUTES.status // 'data-statechart-status'
1940
2305
  STATECHART_ATTRIBUTES.scenario // 'data-statechart-scenario'
1941
2306
 
1942
- 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
1943
2308
  STATECHART_STATUSES.includes('running') // true
1944
2309
  ```
1945
2310
 
1946
- The harness writes the attributes onto its own markup: `status`, `passed`, `failed`, and `total` on
1947
- its root, `scenario` and `result` on each row, `state` on the element rendering the entity's current
1948
- state. A gate reads the root until `status` reads `passed` or `failed`, then reads the tally and
1949
- names each row whose `result` reads `failed`. Neither side spells a `data-statechart-*` string of
1950
- 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.
1951
2324
 
1952
2325
  ### Read a source inventory
1953
2326
 
@@ -2342,6 +2715,89 @@ await traverseAccessible('Evaluate')
2342
2715
  readPerception('Run') // one visible named region, whitespace collapsed, hidden-but-read text kept
2343
2716
  ```
2344
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
+
2345
2801
  ### Drive a field the component listens to
2346
2802
 
2347
2803
  Drive a field by name wherever the keystrokes are part of what the journey claims. Reach for these
@@ -2537,6 +2993,96 @@ The `extractStyles` reading is named for what it returns rather than `extractEsc
2537
2993
  `escape` term already carries the encoding sense in the `@orkestrel/html` and `@orkestrel/console`
2538
2994
  packages.
2539
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
+
2540
3086
  ### Remove an IndexedDB database
2541
3087
 
2542
3088
  Close the connections the test opened, then delete. A live connection blocks the deletion, and the
@@ -2733,7 +3279,12 @@ Each entry names the contracts its file proves. The test names carry the cases.
2733
3279
  exhaustion by attempts and by budget, producer throws counted as attempts with the last one kept as
2734
3280
  the cause, a predicate throw propagated unchanged, and an aborted retry. `waitForEvent` takes the
2735
3281
  exact delivered tuple, a timeout and an abort each naming the cleanup they invoked, and a second
2736
- 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,
2737
3288
  line order, primitive lines, and a malformed physical line named with the native `SyntaxError` as
2738
3289
  its cause. `collect` and `collectStream` drain an empty and an ordered source, and the stream's
2739
3290
  reader lock is released afterwards. `roundTripJSON` takes a copy of a flat and a nested
@@ -2782,7 +3333,33 @@ Each entry names the contracts its file proves. The test names carry the cases.
2782
3333
  straddling an edge. `isReachable` takes a plain control and each condition it drops, a control the
2783
3334
  document no longer holds, a focusable SVG against an element from a foreign namespace, and the
2784
3335
  refused summary that proves it is the one filter the acting verbs apply; `isRendered` takes each
2785
- 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.
3360
+ `readHit` takes a centre that reaches the element itself, a reachable control under a cover that
3361
+ the reading names instead, a soft-wrapped inline target whose two line rectangles leave the box
3362
+ centre on its list item, and a control fixed outside the viewport, whose centre reaches nothing.
2786
3363
  Each acting verb takes its happy path and every voice it owns, including both
2787
3364
  region-scoped refusals and both native-disclosure ones; `clickAccessibleWithin` also takes a
2788
3365
  glyph-captioned control inside a region a glyph-carrying heading labels, which is the loose match
@@ -2888,7 +3465,11 @@ Each entry names the contracts its file proves. The test names carry the cases.
2888
3465
  an uncaught error and an unhandled rejection recorded and then ignored after the stop, the
2889
3466
  channels handed back by identity with a second stop proven a no-op against a replacement, a restart
2890
3467
  that clears `steps` and `output` without stacking wrappers, snapshots that stay what they were, and
2891
- 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.
2892
3473
  - [`tests/src/server/helpers.test.ts`](../tests/src/server/helpers.test.ts) — the `readInventory` and
2893
3474
  wait-family contracts, and each pure leaf against its own inputs. `resolveContained` takes
2894
3475
  contained relative and absolute targets and both spellings of an escape, and `requireContained`
@@ -2960,8 +3541,9 @@ Each entry names the contracts its file proves. The test names carry the cases.
2960
3541
  boundary's uncallable-method and non-object-target refusals, the header flattening, the wait
2961
3542
  family's opposite throw directions with the exhaustion message and its `cause`, the statechart
2962
3543
  table walked against a real disclosure with the failing row's name opening the message and the
2963
- assertion kept as the `cause`, the cookie jar driven against a real origin, and the HTTP upgrade's
2964
- 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.
2965
3547
 
2966
3548
  ## See also
2967
3549