@orkestrel/test 0.0.7 → 0.0.8

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.
@@ -8,6 +8,67 @@
8
8
  */
9
9
  export declare const ACCESSIBLE_ROLES: readonly string[];
10
10
 
11
+ /**
12
+ * Composites one color over another.
13
+ *
14
+ * @param front - The color painted on top.
15
+ * @param back - The color already on the surface.
16
+ * @returns The opaque result a reader sees, its alpha always `1`.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * blendColor([255, 255, 255, 0.5], [0, 0, 0, 1]) // [127.5, 127.5, 127.5, 1]
21
+ * ```
22
+ */
23
+ export declare function blendColor(front: Color, back: Color): Color;
24
+
25
+ /**
26
+ * The page a browser paints an unstyled document onto.
27
+ *
28
+ * @remarks
29
+ * This is the floor a backdrop walk ends on wherever the caller wants the browser's own canvas
30
+ * assumed. `readBackdrop` takes its floor as an argument rather than reaching for this one, so a
31
+ * measurement over a surface the canvas never shows through names the color it actually sits on.
32
+ */
33
+ export declare const CANVAS_COLOR: Color;
34
+
35
+ /**
36
+ * The attribute marking the runner's tester pane, and the rule that sizes it, while a frame is
37
+ * staged.
38
+ *
39
+ * @remarks
40
+ * `stagePane` writes it onto the pane and onto the stylesheet it appends, and `releasePane` finds
41
+ * both by it. Nothing else reads it, so a document carrying it after a capture returned is a pane
42
+ * that was never released.
43
+ */
44
+ export declare const CAPTURE_PANE = "data-capture-pane";
45
+
46
+ /**
47
+ * Shoots one frame at one viewport size and proves the file on disk holds this run's bytes.
48
+ *
49
+ * @param options - The path to write, the viewport to shoot at, and the element to shoot.
50
+ * @returns The absolute path of the written frame, after it has been read back and matched.
51
+ * @throws Thrown when the pane cannot be staged, when the provider wrote the frame somewhere else,
52
+ * and when the bytes on disk are not the ones this shot produced.
53
+ *
54
+ * @remarks
55
+ * The path a screenshot call returns is the path it meant to write, so it is not evidence a file
56
+ * exists. The file is read back through the runner's built-in `readFile` command and compared with
57
+ * the shot itself, which is what separates a frame this run wrote from one an earlier run left
58
+ * behind. The provider resolves `options.path` against the calling test file and returns an absolute
59
+ * path, so the two are compared by the segments that survive resolving `.` and `..` lexically — the
60
+ * refusal is what a provider resolving that path against a different base would trip.
61
+ *
62
+ * Omit `options.element` to shoot the whole page. The pane is staged for the frame and released
63
+ * before this returns, on the failing path as well as the passing one.
64
+ *
65
+ * @example
66
+ * ```ts
67
+ * await captureFrame({ path: '../../tmp/capture/start.png', width: 390, height: 844 })
68
+ * ```
69
+ */
70
+ export declare function captureFrame(options: FrameOptions): Promise<string>;
71
+
11
72
  /** One theme-and-viewport pair a capture run renders. */
12
73
  export declare interface CaptureVariant {
13
74
  /** The variant's name, which is the second half of every filename the run writes. */
@@ -23,6 +84,21 @@ export declare interface CaptureVariant {
23
84
  readonly apply?: () => void;
24
85
  }
25
86
 
87
+ /**
88
+ * Clears both browser storage surfaces.
89
+ *
90
+ * @remarks
91
+ * A browser test file shares one page, so a key written by one test is read by the next one that
92
+ * looks for it. Call this from an `afterEach` hook, which runs after a failed test as well as a
93
+ * passing one, rather than at the end of each test that happens to write a key.
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * afterEach(clearStorage)
98
+ * ```
99
+ */
100
+ export declare function clearStorage(): void;
101
+
26
102
  /**
27
103
  * Clicks one visible, focus-reachable control by its accessible name through the browser provider.
28
104
  *
@@ -76,12 +152,15 @@ export declare function clickAccessibleWithin(region: string, role: string, name
76
152
  *
77
153
  * @param name - The summary text a person reads.
78
154
  * @returns A promise resolving after trusted activation completes.
79
- * @throws When no visible, focus-reachable native summary has that rendered name, or several do.
155
+ * @throws When no native summary with that rendered name passes {@link isReachable}, or several do.
80
156
  *
81
157
  * @remarks
82
158
  * Chromium exposes `<summary>` as a native disclosure rather than through an ARIA role accepted by
83
159
  * `getByRole`, so this resolver names the platform element and its rendered text directly.
84
160
  *
161
+ * It applies the same {@link isReachable} filter the other acting verbs apply, so a summary marked
162
+ * `aria-disabled="true"` is refused here exactly as a button marked that way is refused there.
163
+ *
85
164
  * @example
86
165
  * ```ts
87
166
  * await clickDisclosure('Advanced')
@@ -89,12 +168,34 @@ export declare function clickAccessibleWithin(region: string, role: string, name
89
168
  */
90
169
  export declare function clickDisclosure(name: string): Promise<void>;
91
170
 
171
+ /**
172
+ * One rendered color as straight sRGB channels and its alpha.
173
+ *
174
+ * @remarks
175
+ * The channels run 0–255 and the alpha runs 0–1, which is the shape a computed `rgb()` value already
176
+ * carries. `parseColor` converts the 0–1 channels of `color(srgb …)` onto the same scale, so every
177
+ * color the measurement family passes around is comparable without asking where it came from.
178
+ */
179
+ export declare type Color = readonly [red: number, green: number, blue: number, alpha: number];
180
+
181
+ /**
182
+ * The roles whose accessible name is the text a reader can see inside them.
183
+ *
184
+ * @remarks
185
+ * `readName` reads an element in this list from its own rendered text, after every `aria-hidden`
186
+ * descendant is dropped, and falls through to `title` for every other role.
187
+ */
188
+ export declare const CONTENT_ROLES: readonly string[];
189
+
92
190
  /**
93
191
  * Measures the WCAG 2.x contrast ratio between an element's computed text and background colors.
94
192
  *
95
193
  * @param element - The element whose rendered text contrast to measure.
194
+ * @param floor - The opaque color the backdrop walk ends on. Omit it to refuse a stack the floor
195
+ * would show through instead of assuming one.
96
196
  * @returns The relative-luminance contrast ratio.
97
- * @throws When the browser does not expose parseable computed colors.
197
+ * @throws Thrown when the element exposes no computed foreground color, and — with `floor` omitted
198
+ * — when the walk from the element upwards reaches no opaque layer.
98
199
  *
99
200
  * @remarks
100
201
  * A transparent or translucent background resolves through the element's ancestors: every painted
@@ -103,18 +204,53 @@ export declare function clickDisclosure(name: string): Promise<void>;
103
204
  * full-strength paint. A translucent foreground then resolves against that effective background
104
205
  * before luminance is measured.
105
206
  *
106
- * Every element from the target upwards must be reachable, and at least one of them must paint:
107
- * the measurement throws rather than assuming a white canvas when nothing in the chain declares a
108
- * background color. The element itself must expose a computed foreground color a detached
109
- * element exposes none, and the measurement throws rather than guessing one.
207
+ * With `floor` omitted, the walk from the target upwards must reach a fully opaque layer: the
208
+ * measurement throws rather than assuming a white canvas wherever that canvas would still be part
209
+ * of the answer. The refusal reads the alpha of the deepest layer {@link readLayers} collected, so
210
+ * a chain that declares no background color at all, a chain painting only translucent layers, and a
211
+ * chain deep enough for its composite to round to the canvas's own channels are refused alike,
212
+ * because the number any of them produces is as much a report of the assumption as of the page.
213
+ * Supply a floor wherever the caller knows what the stack sits on — a fragment mounted into a
214
+ * painted host, or a document whose canvas is {@link CANVAS_COLOR} — and the composite is taken
215
+ * over it rather than refused.
216
+ *
217
+ * The element itself must expose a computed foreground color either way. A detached element exposes
218
+ * none, and the measurement throws rather than guessing one.
110
219
  *
111
220
  * @example
112
221
  * ```ts
113
222
  * const container = render('<p style="background: #000; color: #fff">Ready</p>')
114
223
  * contrast(requireValue(container.firstElementChild)) // 21
224
+ * contrast(requireValue(container.firstElementChild), CANVAS_COLOR) // 21, and never refuses
225
+ * ```
226
+ */
227
+ export declare function contrast(element: Element, floor?: Color): number;
228
+
229
+ /**
230
+ * Creates the journal one scenario records its steps and the page's own output into.
231
+ *
232
+ * @returns A journal that records nothing until it is started.
233
+ *
234
+ * @remarks
235
+ * The console is recorded rather than replaced: every intercepted call is forwarded to the channel
236
+ * that was there when the journal started, so a run under a journal prints exactly what it printed
237
+ * without one. `stop` puts those same function references back by identity.
238
+ *
239
+ * Uncaught errors and unhandled rejections are recorded too, through listeners the journal drops
240
+ * when it stops. `steps` and `output` hand out snapshots, so a list read mid-scenario stays what it
241
+ * was. Each journal owns its own recording, so a file that needs one per scenario creates one per
242
+ * scenario.
243
+ *
244
+ * @example
245
+ * ```ts
246
+ * const journal = createJournal()
247
+ * journal.start()
248
+ * journal.record('click', 'Evaluate', 'alerts=0')
249
+ * journal.stop()
250
+ * journal.steps // [{ action: 'click', trigger: 'Evaluate', result: 'alerts=0' }]
115
251
  * ```
116
252
  */
117
- export declare function contrast(element: Element): number;
253
+ export declare function createJournal(): JournalInterface;
118
254
 
119
255
  /**
120
256
  * Creates the capture portfolio one run places its screenshots through.
@@ -130,6 +266,10 @@ export declare function contrast(element: Element): number;
130
266
  * none of it. The portfolio refuses an unregistered variant at creation. An enabled run refuses an
131
267
  * unregistered state name and a second placement of one state.
132
268
  *
269
+ * An enabled `place` writes through `captureFrame`, so a placed state carries that helper's staged
270
+ * pane and its byte readback: a path is recorded only after the file on disk has been proved to hold
271
+ * this run's own frame.
272
+ *
133
273
  * @example
134
274
  * ```ts
135
275
  * const portfolio = createPortfolio({
@@ -143,6 +283,56 @@ export declare function contrast(element: Element): number;
143
283
  */
144
284
  export declare function createPortfolio(options: PortfolioOptions): PortfolioInterface;
145
285
 
286
+ /**
287
+ * Describes the order sequential keyboard navigation visits one element's controls in.
288
+ *
289
+ * @param element - The host to walk; its own controls are described, and it is not itself one.
290
+ * @returns One numbered line per reachable control, naming its role and its name.
291
+ *
292
+ * @remarks
293
+ * A positive `tabindex` is honoured, because a browser honours it: those controls come first in
294
+ * ascending order and everything else follows in document order. A control removed from the
295
+ * sequence by `tabindex="-1"`, by being disabled, or by not being rendered at all is absent here,
296
+ * which is the fact a focus-order verdict is about. A control {@link readRole} answers `undefined`
297
+ * for is named by its lowercased tag, so it is still counted rather than silently dropped.
298
+ *
299
+ * @example
300
+ * ```ts
301
+ * describeFocus(container)
302
+ * // 1. button "Save"
303
+ * // 2. link "Cancel"
304
+ * ```
305
+ */
306
+ export declare function describeFocus(element: Element): string;
307
+
308
+ /**
309
+ * Describes the accessible tree one rendered element presents.
310
+ *
311
+ * @param element - The host to walk, which is described first when it carries a role of its own.
312
+ * @returns One indented line per element carrying a role, naming its role, its name, and its
313
+ * states, in document order; an empty string when nothing in the subtree carries one.
314
+ *
315
+ * @remarks
316
+ * The walk is over the real rendered DOM, so what it reports is the tree the shipped markup and the
317
+ * shipped cascade produce together — a landmark lost to a hidden ancestor is missing here exactly as
318
+ * it is missing for a reader. An element {@link isRendered} refuses is dropped with its whole
319
+ * subtree.
320
+ *
321
+ * Depth follows the roles rather than the elements, so the indentation reads as the structure a
322
+ * screen reader announces instead of as the markup's nesting. An element {@link readRole} answers
323
+ * `undefined` for writes no line and adds no depth, so its children sit where it sat. That is how
324
+ * a wrapper `div` disappears, and it is also how an element {@link IMPLICIT_ROLES} does not answer
325
+ * for disappears — visibly, because its roled children stay at the depth it occupied.
326
+ *
327
+ * @example
328
+ * ```ts
329
+ * describeTree(container)
330
+ * // main "Board"
331
+ * // heading "Totals"
332
+ * ```
333
+ */
334
+ export declare function describeTree(element: Element): string;
335
+
146
336
  /**
147
337
  * Expands a capture registry across every variant into the filenames a complete portfolio holds.
148
338
  *
@@ -163,6 +353,42 @@ export declare function createPortfolio(options: PortfolioOptions): PortfolioInt
163
353
  */
164
354
  export declare function expandCaptures(states: readonly string[], variants: readonly CaptureVariant[]): readonly string[];
165
355
 
356
+ /**
357
+ * Collects every element carrying a component class rendered outside the container it belongs to.
358
+ *
359
+ * @param root - The subtree to sweep.
360
+ * @param child - The component class whose anatomy requires a container, such as `list-group-item`.
361
+ * @param parent - The container class that child class must render inside, such as `list-group`.
362
+ * @returns The markup of every element carrying `child` with no `parent` above it, in document
363
+ * order; an empty list when every one of them is nested correctly.
364
+ *
365
+ * @remarks
366
+ * A component keeps its padding, borders, and radii on the container, so a child class rendered
367
+ * outside one is an unstyled box wearing a component's name, and the interface has to hand-roll the
368
+ * chrome back. The search for the container starts at the element's parent, so an element can never
369
+ * answer the invariant by carrying both classes itself.
370
+ *
371
+ * The class names are arguments, so the check belongs to no framework: name the pair your own
372
+ * cascade defines.
373
+ *
374
+ * @example
375
+ * ```ts
376
+ * extractOrphans(container, 'list-group-item', 'list-group') // []
377
+ * ```
378
+ */
379
+ export declare function extractOrphans(root: ParentNode, child: string, parent: string): readonly string[];
380
+
381
+ /**
382
+ * The role each `input` type carries.
383
+ *
384
+ * @remarks
385
+ * Membership is the contract. The map answers for `button`, `checkbox`, `email`, `number`,
386
+ * `password`, `radio`, `range`, `reset`, `search`, `submit`, `tel`, `text`, and `url`. A type the
387
+ * map omits — `color`, `date`, `file`, `hidden`, and the rest — exposes no role of its own, so
388
+ * `readRole` returns `undefined` for it and `describeTree` writes no line for it.
389
+ */
390
+ export declare const FIELD_ROLES: Readonly<Record<string, string>>;
391
+
166
392
  /**
167
393
  * Replaces a named field's value in one operation, for text too long to type key by key.
168
394
  *
@@ -181,6 +407,61 @@ export declare function expandCaptures(states: readonly string[], variants: read
181
407
  */
182
408
  export declare function fillAccessible(name: string, text: string): Promise<void>;
183
409
 
410
+ /**
411
+ * What sequential keyboard navigation can reach, before disabled and unrendered elements go.
412
+ *
413
+ * @remarks
414
+ * `describeFocus` queries this selector and then drops what a browser drops: an element the
415
+ * accessibility tree does not present, a disabled control, and one removed from the sequence by
416
+ * `tabindex="-1"`. `traverseAccessible` counts the same population to bound its walk, so this is
417
+ * the one list either one reads.
418
+ */
419
+ export declare const FOCUSABLE_SELECTOR = "a[href], area[href], button, input, select, summary, textarea, [tabindex]";
420
+
421
+ /** Options for one captured frame. */
422
+ export declare interface FrameOptions {
423
+ /** The frame's path, relative to the calling test file. */
424
+ readonly path: string;
425
+ /** The viewport width in CSS pixels the frame is shot at. */
426
+ readonly width: number;
427
+ /** The viewport height in CSS pixels the frame is shot at. */
428
+ readonly height: number;
429
+ /** The element to shoot. Omit it to shoot the whole page. */
430
+ readonly element?: Element | undefined;
431
+ }
432
+
433
+ /**
434
+ * The role a `th` carries for the header axis its `scope` names.
435
+ *
436
+ * @remarks
437
+ * A header cell heads a column or a row, and this map answers for the `col` and `row` scopes that
438
+ * say which. A `th` declaring no scope keeps {@link IMPLICIT_ROLES}' `columnheader` rather than the
439
+ * ARIA computation that infers the axis from the table's shape.
440
+ */
441
+ export declare const HEADER_ROLES: Readonly<Record<string, string>>;
442
+
443
+ /**
444
+ * The role each listed tag carries in the accessibility tree when it declares none of its own.
445
+ *
446
+ * @remarks
447
+ * Membership is the contract. The map answers for the sectioning elements `ARTICLE`, `ASIDE`,
448
+ * `FOOTER`, `HEADER`, `MAIN`, `NAV`, `SEARCH`, and `SECTION`; the headings `H1` through `H6`; the
449
+ * grouping and list elements `FIELDSET`, `FORM`, `HR`, `LI`, `OL`, and `UL`; the table elements
450
+ * `TABLE`, `TBODY`, `THEAD`, `TR`, `TD`, and `TH`; and the widgets `BUTTON`, `DIALOG`, `IMG`,
451
+ * `OPTION`, `OUTPUT`, `PROGRESS`, `SUMMARY`, and `TEXTAREA`.
452
+ *
453
+ * A tag the map omits carries no implicit role, so `readRole` returns `undefined` for it,
454
+ * `describeTree` writes no line for it, and the walk continues straight into its children at the
455
+ * depth the omitted element sat at. `A`, `INPUT`, and `SELECT` are absent deliberately: each takes
456
+ * its role from an attribute rather than from its tag, and `readRole` answers for them from their
457
+ * own anatomy.
458
+ *
459
+ * `SECTION` maps to `region`, which `readRole` withholds from an unnamed one, because an unnamed
460
+ * section is not a landmark. `TH` maps to `columnheader`, which {@link HEADER_ROLES} replaces when
461
+ * the cell declares a `scope`.
462
+ */
463
+ export declare const IMPLICIT_ROLES: Readonly<Record<string, string>>;
464
+
184
465
  /**
185
466
  * Determines whether a rectangle lies wholly outside the browser viewport.
186
467
  *
@@ -194,6 +475,161 @@ export declare function fillAccessible(name: string, text: string): Promise<void
194
475
  */
195
476
  export declare function isOutsideViewport(rectangle: DOMRectReadOnly): boolean;
196
477
 
478
+ /**
479
+ * Determines whether a person can click one element where it currently sits.
480
+ *
481
+ * @param element - The element to judge.
482
+ * @returns `true` when the element is connected, visible, laid out with a non-zero box, in the
483
+ * sequential focus order, neither disabled nor marked `aria-disabled="true"`, and outside every
484
+ * `[inert]` subtree; `false` otherwise.
485
+ *
486
+ * @remarks
487
+ * This is the one reachability filter the layer applies. `resolveRendered`, `clickAccessibleWithin`,
488
+ * and `clickDisclosure` each narrow their own candidates and then keep the ones this accepts, so a
489
+ * journey meets one rule rather than three near-copies of it.
490
+ *
491
+ * It measures geometry, which is what separates it from {@link isRendered}. A control clipped to a
492
+ * zero-size rectangle is announced and is not clickable, so `isRendered` accepts it and this
493
+ * refuses it. Nothing here asks about the viewport: `resolveAccessible` scrolls a wholly
494
+ * off-viewport target into view and measures that separately with {@link isOutsideViewport}.
495
+ *
496
+ * @example
497
+ * ```ts
498
+ * isReachable(requireValue(container.querySelector('button')))
499
+ * ```
500
+ */
501
+ export declare function isReachable(element: Element): boolean;
502
+
503
+ /**
504
+ * Determines whether the accessibility tree presents one element at all.
505
+ *
506
+ * @param element - The element to judge.
507
+ * @returns `false` when the element is hidden from assistive technology, from sight, or from both;
508
+ * `true` otherwise.
509
+ *
510
+ * @remarks
511
+ * A control clipped to a zero-size rectangle is still announced, which is the whole point of that
512
+ * idiom, so nothing here reads geometry: only the removals a browser honours — `aria-hidden`
513
+ * anywhere above it, the `hidden` attribute, a hidden input, and a `display` or `visibility` that
514
+ * takes it off the page. {@link isReachable} is the clickable half of the pair and does read
515
+ * geometry.
516
+ *
517
+ * The last two are asked about the element's ancestors as well as itself, which reading a computed
518
+ * `display` cannot do: the computed value of a child of a `display: none` container is the child's
519
+ * own, so a control inside a closed drawer reports itself as laid out. `checkVisibility` answers
520
+ * for the box tree, and `visibility` inherits, so between them an ancestor cannot hide a control
521
+ * from a reader and leave it standing in a description.
522
+ *
523
+ * @example
524
+ * ```ts
525
+ * isRendered(requireValue(container.querySelector('[aria-hidden="true"] button'))) // false
526
+ * ```
527
+ */
528
+ export declare function isRendered(element: Element): boolean;
529
+
530
+ /**
531
+ * The record of one scenario: every step it took and everything the page said while it ran.
532
+ *
533
+ * @remarks
534
+ * Recording is off until {@link JournalInterface.start} arms it, so a suite that never starts a
535
+ * journal pays for none of it. The console is observed by standing in front of it and forwarding
536
+ * every call to the channel that was there: a browser offers no listener for its own output, and a
537
+ * journal that swallowed what it read would hide exactly the diagnostics it exists to keep.
538
+ */
539
+ export declare interface JournalInterface {
540
+ /** Every step recorded since the journal started, in the order it was taken; a snapshot. */
541
+ readonly steps: readonly JournalStep[];
542
+ /** Every console line and uncaught failure the page emitted since it started; a snapshot. */
543
+ readonly output: readonly string[];
544
+ /**
545
+ * Starts a fresh recording, dropping whatever the previous scenario left.
546
+ *
547
+ * @remarks
548
+ * Calling this on a started journal clears both lists and leaves the console interception
549
+ * standing, so a restart never wraps its own wrappers.
550
+ */
551
+ start(): void;
552
+ /**
553
+ * Stops recording and hands every intercepted console channel back by identity.
554
+ *
555
+ * @remarks
556
+ * Calling this on a stopped journal does nothing. The recorded lists survive, so a scenario is
557
+ * read after its recording ends.
558
+ */
559
+ stop(): void;
560
+ /**
561
+ * Records one step, when the journal is started.
562
+ *
563
+ * @param action - What the run did, as one verb.
564
+ * @param trigger - The exact thing it did it to.
565
+ * @param result - What was observed on the surface after the step landed.
566
+ */
567
+ record(action: string, trigger: string, result: string): void;
568
+ }
569
+
570
+ /** One scripted step a journal recorded, and what the surface did about it. */
571
+ export declare interface JournalStep {
572
+ /** What the run did, as one verb. */
573
+ readonly action: string;
574
+ /** The exact thing it did it to. */
575
+ readonly trigger: string;
576
+ /** What was observed on the surface after the step landed. */
577
+ readonly result: string;
578
+ }
579
+
580
+ /**
581
+ * Measures the WCAG 2.x contrast ratio between two opaque colors.
582
+ *
583
+ * @param front - The foreground color, already composited.
584
+ * @param back - The opaque backdrop.
585
+ * @returns The ratio, from `1` for two identical colors to `21` for black against white.
586
+ *
587
+ * @remarks
588
+ * The ratio is symmetric: the brighter of the two luminances is always the numerator, so swapping
589
+ * the arguments returns the same number.
590
+ *
591
+ * @example
592
+ * ```ts
593
+ * measureContrast([0, 0, 0, 1], [255, 255, 255, 1]) // 21
594
+ * ```
595
+ */
596
+ export declare function measureContrast(front: Color, back: Color): number;
597
+
598
+ /**
599
+ * Measures one opaque color's WCAG relative luminance.
600
+ *
601
+ * @param color - The color to weigh. Its alpha is ignored, so composite before calling.
602
+ * @returns The relative luminance, from `0` for black to `1` for white.
603
+ *
604
+ * @example
605
+ * ```ts
606
+ * measureLuminance([255, 255, 255, 1]) // 1
607
+ * ```
608
+ */
609
+ export declare function measureLuminance(color: Color): number;
610
+
611
+ /**
612
+ * Parses one computed CSS color value into straight sRGB channels.
613
+ *
614
+ * @param value - A computed `rgb()`, `rgba()`, or `color(srgb …)` value.
615
+ * @returns The color's channels, or `undefined` when the value names no color this reader speaks.
616
+ *
617
+ * @remarks
618
+ * A computed color resolves to `rgb()` or `rgba()` for every legacy source, and a `color-mix()`
619
+ * declaration resolves to `color(srgb r g b [/ a])` with channels on the 0–1 scale. Both forms are
620
+ * read here and nothing else is: a keyword, a hex triple, an empty string from a detached element,
621
+ * and a color space the cascade never hands back all return `undefined`. Absence is the answer
622
+ * rather than a transparent color, so a caller decides what an unreadable value means instead of
623
+ * measuring a black it never saw.
624
+ *
625
+ * @example
626
+ * ```ts
627
+ * parseColor('rgba(255, 255, 255, 0.5)') // [255, 255, 255, 0.5]
628
+ * parseColor('rebeccapurple') // undefined
629
+ * ```
630
+ */
631
+ export declare function parseColor(value: string): Color | undefined;
632
+
197
633
  /** The registry of capture states one run places, and the files it wrote placing them. */
198
634
  export declare interface PortfolioInterface {
199
635
  /** The name of the variant this run renders. */
@@ -205,14 +641,16 @@ export declare interface PortfolioInterface {
205
641
  /** The registry expanded across every variant: the filenames a complete portfolio holds. */
206
642
  readonly files: readonly string[];
207
643
  /**
208
- * Places one registered state: applies the variant, resizes the viewport, and writes the
644
+ * Places one registered state: applies the variant, stages the pane, and writes the verified
209
645
  * screenshot.
210
646
  *
211
647
  * @param state - The state name from the registry.
648
+ * @param element - The element to shoot. Omit it to shoot the whole page.
212
649
  * @returns The written path, or `undefined` when the portfolio is not enabled.
213
- * @throws When the state is not registered or has already been placed.
650
+ * @throws When the state is not registered, has already been placed, or the written frame does
651
+ * not read back as the bytes this shot produced.
214
652
  */
215
- place(state: string): Promise<string | undefined>;
653
+ place(state: string, element?: Element): Promise<string | undefined>;
216
654
  }
217
655
 
218
656
  /** Options for a capture portfolio. */
@@ -248,6 +686,32 @@ export declare interface PortfolioOptions {
248
686
  */
249
687
  export declare function pressKeys(keys: string): Promise<void>;
250
688
 
689
+ /**
690
+ * Resolves the opaque color standing behind one element.
691
+ *
692
+ * @param element - The element whose backdrop to resolve.
693
+ * @param floor - The opaque color the walk ends on when nothing above it paints.
694
+ * @returns The composited color a reader sees behind the element.
695
+ *
696
+ * @remarks
697
+ * The layers {@link readLayers} collects composite top-over-bottom onto the floor, so a 3% surface
698
+ * tint reads as a tint over what shows through it rather than as a full-strength paint.
699
+ *
700
+ * The floor is required, because this leaf never guesses what a document sits on. Pass
701
+ * {@link CANVAS_COLOR} for the page a browser paints behind an unstyled document, or the color of
702
+ * the surface a fragment is really rendered into. When no layer paints, the floor is returned by
703
+ * identity.
704
+ *
705
+ * The composite alone never says whether the floor is part of the answer. A caller that must know
706
+ * reads the stack instead.
707
+ *
708
+ * @example
709
+ * ```ts
710
+ * readBackdrop(requireValue(container.querySelector('p')), CANVAS_COLOR)
711
+ * ```
712
+ */
713
+ export declare function readBackdrop(element: Element, floor: Color): Color;
714
+
251
715
  /**
252
716
  * Collects every class token the stylesheets loaded into this document actually define.
253
717
  *
@@ -279,6 +743,56 @@ export declare function readCascade(): ReadonlySet<string>;
279
743
  */
280
744
  export declare function readFocus(): string | undefined;
281
745
 
746
+ /**
747
+ * Collects the painted layers standing between one element and the surface it sits on.
748
+ *
749
+ * @param element - The element to walk up from.
750
+ * @returns Every layer the walk paints, the element's own first and the deepest last.
751
+ *
752
+ * @remarks
753
+ * A surface token paints one ancestor while every element between it and the text paints nothing,
754
+ * so a backdrop is found by walking up rather than by reading the element's own `background-color`,
755
+ * which is almost always transparent. A fully transparent layer paints nothing and is left out, and
756
+ * the walk stops at the first fully opaque layer, because nothing above that layer is visible.
757
+ *
758
+ * The stack is what tells a resolved backdrop from an assumed one: the walk reached an opaque
759
+ * surface exactly when its last layer's alpha is `1`. {@link contrast} refuses on that reading,
760
+ * which no comparison of composited colors can replace — 64 half-transparent layers composite to
761
+ * the same channels over opposite floors, because the floor's remaining share falls below the last
762
+ * bit a channel carries.
763
+ *
764
+ * @example
765
+ * ```ts
766
+ * readLayers(requireValue(container.querySelector('p')))
767
+ * ```
768
+ */
769
+ export declare function readLayers(element: Element): readonly Color[];
770
+
771
+ /**
772
+ * Reads the accessible name one element is announced under.
773
+ *
774
+ * @param element - The element to name.
775
+ * @returns The computed name, or an empty string when the element carries none.
776
+ *
777
+ * @remarks
778
+ * The order is the one a browser follows: `aria-labelledby`, then `aria-label`, then a form
779
+ * control's own labels, then an image's `alt`, then the text inside a role {@link CONTENT_ROLES}
780
+ * names, then `title`. A submit, reset, or button input is named by its value, because it renders
781
+ * no text to read. An `aria-labelledby` naming several ids joins their texts in the order the
782
+ * attribute lists them, and an id nothing answers for is skipped rather than fatal.
783
+ *
784
+ * Each step answers only when it has something to say, so a step that carries nothing hands the
785
+ * element to the next one. An image whose `alt` is absent or blank is the case that shows it:
786
+ * `<img title="Chart">` is named `Chart` rather than the empty string its own `alt` step would
787
+ * have returned, and an image carrying both keeps answering `alt`.
788
+ *
789
+ * @example
790
+ * ```ts
791
+ * readName(requireValue(container.querySelector('button'))) // 'Save changes'
792
+ * ```
793
+ */
794
+ export declare function readName(element: Element): string;
795
+
282
796
  /**
283
797
  * Reads the normalized visible text of the whole page.
284
798
  *
@@ -312,6 +826,66 @@ export declare function readPage(): string;
312
826
  */
313
827
  export declare function readPerception(name: string): string;
314
828
 
829
+ /**
830
+ * Measures the contrast the focus chrome painted on one control reaches against its own backdrop.
831
+ *
832
+ * @param control - The control that holds the focus.
833
+ * @param worn - The element the control's focus chrome is painted onto. Default: `control`.
834
+ * @returns The strongest ratio the painted focus chrome reaches, or `undefined` when the control is
835
+ * not showing `:focus-visible` or the cascade paints no chrome of its own.
836
+ *
837
+ * @remarks
838
+ * This reads and never acts. Focus arrives through the published verbs — `traverseAccessible`,
839
+ * `pressKeys`, a real click — and this measures what the browser painted once it landed. A control
840
+ * that is not matching `:focus-visible` when the call is made reports nothing, because no
841
+ * measurement taken then would be about focus.
842
+ *
843
+ * Some controls are two elements: one that takes the focus and one a reader can see. A hidden radio
844
+ * beside the label that carries every pixel of its chrome is the case `worn` exists for, so a
845
+ * measurement is not taken on a rectangle nobody is looking at. The focus state is still read off
846
+ * `control`, because that is what holds it.
847
+ *
848
+ * The backdrop is the surface behind the element the chrome is worn on, resolved from that element's
849
+ * parent through {@link readBackdrop} onto {@link CANVAS_COLOR}. A control whose ancestry paints
850
+ * nothing is therefore measured against the browser's own canvas, which is what a reader looking at
851
+ * an unstyled document sees.
852
+ *
853
+ * Only chrome the cascade paints is measured — an `outline` with a real style and width, and the
854
+ * first color in a `box-shadow`. A control left the browser's own `outline-style: auto` ring reports
855
+ * `undefined`, because that ring's two tones are guaranteed against any backdrop and its computed
856
+ * color names neither. A focus style that only changes the control's own fill reports `undefined`
857
+ * too: the resting fill is gone by the time focus is on the control, and this never moves focus to
858
+ * go and read it.
859
+ *
860
+ * @example
861
+ * ```ts
862
+ * await traverseAccessible('Evaluate')
863
+ * readRing(resolveRendered('Evaluate')) // the ratio the painted ring reaches
864
+ * ```
865
+ */
866
+ export declare function readRing(control: Element, worn?: Element): number | undefined;
867
+
868
+ /**
869
+ * Reads the role one element carries in the accessibility tree.
870
+ *
871
+ * @param element - The element to classify.
872
+ * @returns The declared role, the implicit one, or `undefined` when the element carries none.
873
+ *
874
+ * @remarks
875
+ * A declared `role` wins outright, and its first token is the answer when several are listed.
876
+ * Otherwise the element's own anatomy decides: an anchor is a link only while it holds an `href`,
877
+ * an `input` takes the role {@link FIELD_ROLES} gives its type, a `select` is a combobox until it
878
+ * offers several rows at once, a `section` is a region only once something names it, and a `th`
879
+ * heads whichever axis its `scope` names. Every other tag answers from {@link IMPLICIT_ROLES},
880
+ * whose membership is the contract for what this can answer at all.
881
+ *
882
+ * @example
883
+ * ```ts
884
+ * readRole(requireValue(container.querySelector('a[href]'))) // 'link'
885
+ * ```
886
+ */
887
+ export declare function readRole(element: Element): string | undefined;
888
+
315
889
  /**
316
890
  * Reads the normalized visible text of every element a selector matches, in document order.
317
891
  *
@@ -331,6 +905,47 @@ export declare function readPerception(name: string): string;
331
905
  */
332
906
  export declare function readRows(root: ParentNode, selector: string): readonly string[];
333
907
 
908
+ /**
909
+ * Reads the states one element is announced in.
910
+ *
911
+ * @param element - The element to read.
912
+ * @returns Every state the element declares, in one fixed order.
913
+ *
914
+ * @remarks
915
+ * A state a reader is told about is one this records: what is unavailable, disclosed, pressed,
916
+ * current, refused, chosen, announcing itself, demanded, uneditable, described, or busy. The order
917
+ * is fixed, so two descriptions of the same surface are comparable line for line.
918
+ *
919
+ * A native disclosure states its expansion on the parent `details` element's own `open` rather than
920
+ * on an ARIA attribute, so a summary that declares no `aria-expanded` is read from the platform's
921
+ * one copy of that fact.
922
+ *
923
+ * @example
924
+ * ```ts
925
+ * readStates(requireValue(container.querySelector('summary'))) // ['collapsed']
926
+ * ```
927
+ */
928
+ export declare function readStates(element: Element): readonly string[];
929
+
930
+ /**
931
+ * Reads one element's rendered text the way a name computation reads it.
932
+ *
933
+ * @param element - The element whose announced words are wanted.
934
+ * @returns The text with every `aria-hidden` descendant dropped and whitespace runs collapsed.
935
+ *
936
+ * @remarks
937
+ * A glyph marked `aria-hidden` contributes nothing to a name, so a control captioned by an icon
938
+ * plus a word reads as the word alone — which is what a reader hears, and what a verdict citing a
939
+ * description has to compare against the copy a template writes. Reach for `readRows` wherever the
940
+ * subject is what the page paints rather than what it announces: that one keeps the glyph.
941
+ *
942
+ * @example
943
+ * ```ts
944
+ * readText(requireValue(container.querySelector('button'))) // 'Save'
945
+ * ```
946
+ */
947
+ export declare function readText(element: Element): string;
948
+
334
949
  /**
335
950
  * Reads the value a resolved control renders.
336
951
  *
@@ -350,6 +965,24 @@ export declare function readRows(root: ParentNode, selector: string): readonly s
350
965
  */
351
966
  export declare function readValue(role: string, name: string): string;
352
967
 
968
+ /**
969
+ * Hands the tester pane back to the runner's own layout.
970
+ *
971
+ * @remarks
972
+ * A staged pane is the runner's fitting scale suppressed, so a pane left staged outlives the capture
973
+ * that needed it and every later act in the file happens on a surface the runner is no longer
974
+ * fitting to its window. What that costs is not a wrong picture: it is a control whose page
975
+ * coordinates fall outside the pane, which the runner's own layout then intercepts, so an ordinary
976
+ * press fails with the voice of a control that is covered. Calling this on an unstaged pane does
977
+ * nothing.
978
+ *
979
+ * @example
980
+ * ```ts
981
+ * releasePane()
982
+ * ```
983
+ */
984
+ export declare function releasePane(): void;
985
+
353
986
  /**
354
987
  * Renders trusted fixture markup into a container attached to the document.
355
988
  *
@@ -421,6 +1054,47 @@ export declare function resolveAccessible(role: string, name: string): HTMLEleme
421
1054
  */
422
1055
  export declare function resolveRendered(first: string, second?: string): HTMLElement;
423
1056
 
1057
+ /**
1058
+ * Sets the tester's viewport and renders the runner's pane at the size that viewport claims.
1059
+ *
1060
+ * @param width - The viewport width in CSS pixels.
1061
+ * @param height - The viewport height in CSS pixels.
1062
+ * @returns A promise resolving after the resized pane has been painted.
1063
+ * @throws Thrown when the tester sits inside no pane a capture can size, and when the staged pane
1064
+ * does not render at the viewport it was given.
1065
+ *
1066
+ * @remarks
1067
+ * This depends on the runner's own tester layout, and that dependency is contract rather than an
1068
+ * accident: `vitest@4.1.11` lays its tester out inside a smaller page, fits it by scaling the pane
1069
+ * the tester sits in, and clips whatever overflows that pane. Layout inside the tester is
1070
+ * unaffected — the tester reports the viewport it was given and every breakpoint answers to it —
1071
+ * but a screenshot is taken off the page the runner painted, so a frame shot through that scale is
1072
+ * a thumbnail of the surface and a frame shot after only unscaling it is a sliver. The tester is
1073
+ * therefore unscaled and lifted to the window's own origin for the shot. The `iframe[data-vitest]`
1074
+ * selector and the `--tester-transform`, `--tester-margin-left`, `--viewport-width`, and
1075
+ * `--viewport-height` custom properties are the runner's, so a Vitest release that renames any of
1076
+ * them reddens the size check below rather than writing a wrong frame.
1077
+ *
1078
+ * Hand the pane straight back with {@link releasePane}. A tester pinned at a viewport taller than
1079
+ * the window puts its lower half beyond what a pointer can reach, so an ordinary press then fails
1080
+ * as a control outside the viewport, in a test that took no picture at all.
1081
+ *
1082
+ * The rule is declared rather than written inline, because the runner writes its own scale onto the
1083
+ * pane as inline custom properties and rewrites them whenever the tester resizes. A declared rule
1084
+ * marked important outranks an inline value and survives every rewrite. It finds the pane by the
1085
+ * tester it contains as well as by {@link CAPTURE_PANE}, because a re-render between the staging and
1086
+ * the shot replaces the node and takes any attribute of ours with it.
1087
+ *
1088
+ * The wait is two frames rather than a delay: the first carries the resize into layout and the
1089
+ * second is the paint a screenshot reads.
1090
+ *
1091
+ * @example
1092
+ * ```ts
1093
+ * await stagePane(390, 844)
1094
+ * ```
1095
+ */
1096
+ export declare function stagePane(width: number, height: number): Promise<void>;
1097
+
424
1098
  /**
425
1099
  * Reads one resolved CSS property from a real browser element.
426
1100
  *