@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.
- package/dist/bin/main.js +75 -34
- package/dist/bin/main.js.map +1 -1
- package/dist/host/agents/skills/orkestrel-prove-journey/SKILL.md +175 -29
- package/dist/host/agents/skills/orkestrel-prove-journey/references/captures.md +42 -15
- package/dist/host/agents/skills/orkestrel-prove-journey/references/decide.md +34 -15
- package/dist/host/agents/skills/orkestrel-prove-journey/references/layer.md +248 -52
- package/dist/host/agents/skills/orkestrel-prove-journey/references/statechart.md +255 -49
- package/dist/host/agents/skills/orkestrel-prove-journey/references/styles.md +111 -23
- package/dist/host/agents/transports/codex.md +7 -0
- package/dist/host/claude/agents/orkestrel.md +46 -46
- package/dist/host/claude/rules/documentation.md +2 -0
- package/dist/host/claude/rules/tests.md +5 -3
- package/dist/host/claude/rules/workspace.md +26 -15
- package/dist/host/dotfiles/gitignore +3 -0
- package/dist/host/guides/README.md +5 -0
- package/dist/host/guides/scaffold.md +97 -13
- package/dist/host/guides/test.md +788 -206
- package/dist/host/manifest.json +22 -32
- package/dist/host/tests/config.test.ts +272 -121
- package/dist/host/tests/policy.test.ts +11 -1
- package/dist/host/tests/setupPolicy.ts +571 -7
- package/dist/src/core/index.cjs +109 -18
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +19 -6
- package/dist/src/core/index.d.ts +19 -6
- package/dist/src/core/index.js +109 -19
- package/dist/src/core/index.js.map +1 -1
- package/package.json +2 -2
package/dist/host/guides/test.md
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
|
114
|
-
| -------------------------- | --------- |
|
|
115
|
-
| `WaitOptions` | interface | `{ budget?, interval?, signal? }`
|
|
116
|
-
| `RetryOptions` | interface | `WaitOptions` plus `{ attempts? }`
|
|
117
|
-
| `
|
|
118
|
-
| `
|
|
119
|
-
| `
|
|
120
|
-
| `
|
|
121
|
-
| `
|
|
122
|
-
| `
|
|
123
|
-
| `
|
|
124
|
-
| `
|
|
125
|
-
| `
|
|
126
|
-
| `
|
|
127
|
-
| `
|
|
128
|
-
| `
|
|
129
|
-
| `
|
|
130
|
-
| `
|
|
131
|
-
| `
|
|
132
|
-
| `
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
|
152
|
-
|
|
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; `
|
|
223
|
-
|
|
224
|
-
caller
|
|
225
|
-
`
|
|
226
|
-
`
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
|
241
|
-
|
|
|
242
|
-
| `Color`
|
|
243
|
-
| `ElementOptions`
|
|
244
|
-
| `FrameOptions`
|
|
245
|
-
| `FrameReading`
|
|
246
|
-
| `CaptureVariant`
|
|
247
|
-
| `PortfolioOptions`
|
|
248
|
-
| `PortfolioInterface`
|
|
249
|
-
| `JournalStep`
|
|
250
|
-
| `JournalInterface`
|
|
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
|
|
271
|
-
| ----------------------- | -------- |
|
|
272
|
-
| `resolveAccessible` | function | `(name: string) => HTMLElement` / `(role: string, name: string) => HTMLElement`
|
|
273
|
-
| `resolveRendered` | function | `(first: string, second?: string) => HTMLElement`
|
|
274
|
-
| `computeNamePattern` | function | `(name: string) => RegExp`
|
|
275
|
-
| `isOutsideViewport` | function | `(rectangle: DOMRectReadOnly) => boolean`
|
|
276
|
-
| `isRendered` | function | `(element: Element) => boolean`
|
|
277
|
-
| `isReachable` | function | `(element: Element) => boolean`
|
|
278
|
-
| `
|
|
279
|
-
| `
|
|
280
|
-
| `
|
|
281
|
-
| `
|
|
282
|
-
| `
|
|
283
|
-
| `
|
|
284
|
-
| `
|
|
285
|
-
| `
|
|
286
|
-
| `
|
|
287
|
-
| `
|
|
288
|
-
| `
|
|
289
|
-
| `
|
|
290
|
-
| `
|
|
291
|
-
| `
|
|
292
|
-
| `
|
|
293
|
-
| `
|
|
294
|
-
| `
|
|
295
|
-
| `
|
|
296
|
-
| `
|
|
297
|
-
| `
|
|
298
|
-
| `
|
|
299
|
-
| `
|
|
300
|
-
| `
|
|
301
|
-
| `
|
|
302
|
-
| `
|
|
303
|
-
| `
|
|
304
|
-
| `
|
|
305
|
-
| `
|
|
306
|
-
| `
|
|
307
|
-
| `
|
|
308
|
-
| `
|
|
309
|
-
| `
|
|
310
|
-
| `
|
|
311
|
-
| `
|
|
312
|
-
| `
|
|
313
|
-
| `
|
|
314
|
-
| `
|
|
315
|
-
| `
|
|
316
|
-
| `
|
|
317
|
-
| `
|
|
318
|
-
| `
|
|
319
|
-
| `
|
|
320
|
-
| `
|
|
321
|
-
| `
|
|
322
|
-
| `
|
|
323
|
-
| `
|
|
324
|
-
| `
|
|
325
|
-
| `
|
|
326
|
-
| `
|
|
327
|
-
| `
|
|
328
|
-
| `
|
|
329
|
-
| `
|
|
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
|
|
932
|
-
|
|
|
933
|
-
| `No interactive element has the accessible name "<name>"`
|
|
934
|
-
| `Interactive target "<name>" is not visible and focus-reachable`
|
|
935
|
-
| `Interactive target "<name>" is ambiguous across <n> elements`
|
|
936
|
-
| `Interactive target "<name>" could not be resolved`
|
|
937
|
-
| `Interactive target "<name>" is unreachable after scrolling`
|
|
938
|
-
| `Interactive target "<name>" is not reachable inside "<region>"`
|
|
939
|
-
| `Interactive target "<name>" is ambiguous across <n> elements inside "<region>"`
|
|
940
|
-
| `Interactive target "<name>" could not be resolved inside "<region>"`
|
|
941
|
-
| `Native disclosure "<name>" is not visible and focus-reachable`
|
|
942
|
-
| `Native disclosure "<name>" is ambiguous across <n> elements`
|
|
943
|
-
| `Native disclosure "<name>" could not be resolved`
|
|
944
|
-
| `Interactive target "<name>" is not reachable through forward Tab traversal: <trail>`
|
|
945
|
-
| `Named region "<name>" is not visible`
|
|
946
|
-
| `Named region "<name>" is ambiguous across <n> elements`
|
|
947
|
-
| `Named region "<name>" could not be resolved`
|
|
948
|
-
| `Interactive target "<name>" does not carry a value`
|
|
949
|
-
| `Computed foreground color is unavailable`
|
|
950
|
-
| `Computed background color is unavailable`
|
|
951
|
-
| `Tester pane is unavailable for a capture`
|
|
952
|
-
| `Tester pane rendered <w>x<h> for a <w>x<h> viewport`
|
|
953
|
-
| `Capture frame at <path> never settled after <n> restagings: <h> over a <h> pane`
|
|
954
|
-
| `Capture frame was written to <path> where <path> was asked for`
|
|
955
|
-
| `Capture frame at <path> is not the one this run shot`
|
|
956
|
-
| `Capture frame at <path> could not be read`
|
|
957
|
-
| `Capture frame at <path> is not an image this browser decodes`
|
|
958
|
-
| `Capture frame at <path> cannot be measured without a 2D canvas`
|
|
959
|
-
| `Capture variant "<name>" is not registered`
|
|
960
|
-
| `Capture state "<state>" is not registered`
|
|
961
|
-
| `Capture state "<state>" is already placed`
|
|
962
|
-
| `IndexedDB database "<name>" could not be deleted`
|
|
963
|
-
| `IndexedDB database "<name>" is blocked by an open connection`
|
|
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. **
|
|
1146
|
-
|
|
1147
|
-
|
|
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
|
|
1160
|
-
|
|
1161
|
-
the test runner and
|
|
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`,
|
|
1192
|
-
`
|
|
1193
|
-
`readContrast`, `readLayers`, `readBackdrop`, and `readRing` — and
|
|
1194
|
-
the caller already has rather than a verb that acts on a target.
|
|
1195
|
-
one as the subject of a
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
the
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
`
|
|
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. `
|
|
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 |
|
|
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.
|
|
1859
|
-
|
|
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 = '
|
|
2098
|
+
type DisclosureEvent = 'toggle' | 'dismiss'
|
|
1868
2099
|
|
|
1869
2100
|
interface DisclosureContext {
|
|
1870
|
-
readonly
|
|
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: {
|
|
1877
|
-
|
|
1878
|
-
|
|
2146
|
+
transition: {
|
|
2147
|
+
name: 'closed opens through the summary',
|
|
2148
|
+
from: 'closed',
|
|
2149
|
+
event: 'toggle',
|
|
2150
|
+
to: 'open',
|
|
1879
2151
|
},
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
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
|
-
|
|
1885
|
-
|
|
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,
|
|
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.
|
|
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
|
-
|
|
1912
|
-
//
|
|
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,
|
|
1917
|
-
// Error:
|
|
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:
|
|
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
|
-
|
|
1934
|
-
|
|
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
|
|
2307
|
+
STATECHART_STATUSES[0] // 'pending' — carried until every declared row has rendered
|
|
1943
2308
|
STATECHART_STATUSES.includes('running') // true
|
|
1944
2309
|
```
|
|
1945
2310
|
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
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. `
|
|
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
|
|
2964
|
-
|
|
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
|
|