@orkestrel/test 0.0.15 → 0.0.17
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/README.md +7 -5
- package/dist/src/browser/index.d.ts +677 -8
- package/dist/src/browser/index.js +686 -1
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +99 -16
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +580 -456
- package/dist/src/core/index.d.ts +580 -456
- package/dist/src/core/index.js +98 -17
- package/dist/src/core/index.js.map +1 -1
- package/package.json +5 -5
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isError, isString } from "@orkestrel/contract";
|
|
2
|
+
import { STATECHART_ATTRIBUTES, STATECHART_STATUSES, buildRefusal, checkBounds, executeScenario, requireValue, waitForAbort, waitForCondition, waitForDelay } from "../core/index.js";
|
|
1
3
|
import { commands, page, userEvent } from "vitest/browser";
|
|
2
4
|
//#region src/browser/constants.ts
|
|
3
5
|
/**
|
|
@@ -224,6 +226,13 @@ function isOutsideViewport(rectangle) {
|
|
|
224
226
|
* refuses it. Nothing here asks about the viewport: `resolveAccessible` scrolls a wholly
|
|
225
227
|
* off-viewport target into view and measures that separately with {@link isOutsideViewport}.
|
|
226
228
|
*
|
|
229
|
+
* Inside a shadow tree it answers for the element's own facts, in an open root and a closed one
|
|
230
|
+
* alike: the box, the focus order, `:disabled`, and `aria-disabled` are all the element's. The
|
|
231
|
+
* `[inert]` ancestor is the one read that stops at the boundary, because `closest` never leaves the
|
|
232
|
+
* element's own tree, so a host marked `[inert]` is invisible here. What the flat tree decides still
|
|
233
|
+
* reaches the subject — a host the document does not lay out takes the element off the page and this
|
|
234
|
+
* refuses it. Ask the host separately where an ancestor attribute is the subject.
|
|
235
|
+
*
|
|
227
236
|
* @example
|
|
228
237
|
* ```ts
|
|
229
238
|
* isReachable(requireValue(container.querySelector('button')))
|
|
@@ -256,6 +265,13 @@ function isReachable(element) {
|
|
|
256
265
|
* for the box tree, and `visibility` inherits, so between them an ancestor cannot hide a control
|
|
257
266
|
* from a reader and leave it standing in a description.
|
|
258
267
|
*
|
|
268
|
+
* Inside a shadow tree it answers for the element's own facts, in an open root and a closed one
|
|
269
|
+
* alike. The `aria-hidden` ancestor is the one read that stops at the boundary, because `closest`
|
|
270
|
+
* never leaves the element's own tree, so a host marked `aria-hidden="true"` is invisible here and
|
|
271
|
+
* this reports `true` for a subject a reader is never told about. `checkVisibility` and the computed
|
|
272
|
+
* `visibility` read the flat tree, so a host the document does not lay out still takes the element
|
|
273
|
+
* off the page. Ask the host separately where an ancestor attribute is the subject.
|
|
274
|
+
*
|
|
259
275
|
* @example
|
|
260
276
|
* ```ts
|
|
261
277
|
* isRendered(requireValue(container.querySelector('[aria-hidden="true"] button'))) // false
|
|
@@ -269,6 +285,67 @@ function isRendered(element) {
|
|
|
269
285
|
return getComputedStyle(element).visibility !== "hidden";
|
|
270
286
|
}
|
|
271
287
|
/**
|
|
288
|
+
* Reads the topmost element at one element's bounding-box centre.
|
|
289
|
+
*
|
|
290
|
+
* @param element - The element whose bounding-box centre is the point to read.
|
|
291
|
+
* @returns The element the owner document's hit test names at that point, or `undefined` where the
|
|
292
|
+
* point lies outside that viewport or reaches nothing.
|
|
293
|
+
*
|
|
294
|
+
* @remarks
|
|
295
|
+
* This reads one point and nothing else: the centre of the element's own bounding box, hit-tested
|
|
296
|
+
* against `element.ownerDocument`. That point is where a thumb aimed at the middle of what it sees
|
|
297
|
+
* lands, and the arrangements that take it away are the ones {@link isReachable} cannot see: a
|
|
298
|
+
* sticky masthead covering a control that was scrolled to, and a wrapped inline target, whose
|
|
299
|
+
* per-line rectangles leave a gap the single bounding box spans and whose centre falls in that gap
|
|
300
|
+
* on the ancestor. `isReachable` reads `checkVisibility`, geometry, and the focus order, and each
|
|
301
|
+
* arrangement passes all three while a click at that point misses.
|
|
302
|
+
*
|
|
303
|
+
* It does not predict where the installed driver clicks. `playwright-core@1.63.0` clips each
|
|
304
|
+
* content quad to the viewport, drops every quad left without area, and takes the midpoint of the
|
|
305
|
+
* first quad that survives — `_clickablePoint` at `playwright-core/lib/coreBundle.js:20084`,
|
|
306
|
+
* reached from the locator `click` the Vitest provider delegates to. For the wrapped target the
|
|
307
|
+
* first surviving quad is the first line box, so the driver aims inside the link while this centre
|
|
308
|
+
* sits in the gap. Read a result as the answer for the point this names, and for no other.
|
|
309
|
+
*
|
|
310
|
+
* It returns the node rather than a verdict, because the node is the diagnosis: a caller narrows
|
|
311
|
+
* the result, rules on `link.contains(hit)`, and names what came back — the list item rather than
|
|
312
|
+
* the link, the masthead rather than the control.
|
|
313
|
+
*
|
|
314
|
+
* Pass {@link isRendered} and {@link isReachable} before reading, because neither answer means
|
|
315
|
+
* anything on an element that failed them. An element the document does not render measures a zero
|
|
316
|
+
* rectangle at the origin, and a zero-area element measures a point on its own edge; each is
|
|
317
|
+
* hit-tested like any other point and names whatever paints there — the surrounding container for
|
|
318
|
+
* a control clipped inside one, and the document body for a rectangle collapsed at the origin.
|
|
319
|
+
* `contains` is then false, and a caller that skipped the gates reports a cover that is not there.
|
|
320
|
+
*
|
|
321
|
+
* A returned node carries silences of its own. A cover painted with `pointer-events: none` is
|
|
322
|
+
* absent from the hit test, so the reading names the element underneath it and the caller reads
|
|
323
|
+
* reachable for a cover a person can see. An element inside a shadow tree retargets, in an open
|
|
324
|
+
* root and a closed one alike: the document-level hit test names the host, the inner element does
|
|
325
|
+
* not contain the host, and the caller reads the element's own host as a cover. Ask
|
|
326
|
+
* `element.getRootNode()` for its own `elementFromPoint` where the subject sits in a shadow tree.
|
|
327
|
+
*
|
|
328
|
+
* `undefined` carries one silence: a centre outside the viewport reads the same as a centre that
|
|
329
|
+
* reaches nothing. {@link isOutsideViewport} does not separate them, because it asks whether the
|
|
330
|
+
* whole rectangle misses the viewport while this asks where one point lands — a rectangle at
|
|
331
|
+
* `left: -80` with `width: 100` has its right edge at 20, so that predicate reports false while the
|
|
332
|
+
* centre at -30 reads `undefined` here. The two also measure different windows: the predicate reads
|
|
333
|
+
* the global `window`, and this reads `element.ownerDocument`. Compare the centre against that
|
|
334
|
+
* document's own viewport where the distinction is the subject.
|
|
335
|
+
*
|
|
336
|
+
* @example
|
|
337
|
+
* ```ts
|
|
338
|
+
* const link = requireValue(container.querySelector('a'))
|
|
339
|
+
* const hit = readHit(link)
|
|
340
|
+
* // False for a wrapped link whose centre sits between its line boxes.
|
|
341
|
+
* hit !== undefined && link.contains(hit)
|
|
342
|
+
* ```
|
|
343
|
+
*/
|
|
344
|
+
function readHit(element) {
|
|
345
|
+
const rectangle = element.getBoundingClientRect();
|
|
346
|
+
return element.ownerDocument.elementFromPoint(rectangle.left + rectangle.width / 2, rectangle.top + rectangle.height / 2) ?? void 0;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
272
349
|
* Computes the pattern that matches one accessible name a decorative glyph may sit beside.
|
|
273
350
|
*
|
|
274
351
|
* @param name - The exact accessible name a person reads, whitespace runs collapsed on the way in.
|
|
@@ -470,6 +547,35 @@ async function fillAccessible(name, text) {
|
|
|
470
547
|
await userEvent.fill(resolveRendered(name), text);
|
|
471
548
|
}
|
|
472
549
|
/**
|
|
550
|
+
* Sends a key sequence to whatever holds focus, and refuses to send it to nothing.
|
|
551
|
+
*
|
|
552
|
+
* @param keys - The sequence in the provider's own key syntax, such as `{Enter}` or `{Escape}`.
|
|
553
|
+
* @returns A promise resolving after every keystroke completes.
|
|
554
|
+
* @throws When the document body holds focus, or nothing does.
|
|
555
|
+
*
|
|
556
|
+
* @remarks
|
|
557
|
+
* The refusal is the whole of what this adds over `userEvent.keyboard`. A key sent while focus sits
|
|
558
|
+
* on the body reaches no control, and every assertion after it reads the surface the key never
|
|
559
|
+
* touched — which is the false green a guarded keyboard step exists to catch. Bring focus about
|
|
560
|
+
* first through {@link traverseAccessible}, {@link clickAccessible}, or {@link typeAccessible}, and
|
|
561
|
+
* send the sequence here.
|
|
562
|
+
*
|
|
563
|
+
* Escaping is the caller's, because the sequence is the subject: `{` opens a key name and `[` opens
|
|
564
|
+
* a code name. Reach for {@link typeAccessible} where the text is the subject and the syntax is in
|
|
565
|
+
* the way.
|
|
566
|
+
*
|
|
567
|
+
* @example
|
|
568
|
+
* ```ts
|
|
569
|
+
* await traverseAccessible('Evaluate')
|
|
570
|
+
* await pressKeys('{Enter}')
|
|
571
|
+
* ```
|
|
572
|
+
*/
|
|
573
|
+
async function pressKeys(keys) {
|
|
574
|
+
const focused = document.activeElement;
|
|
575
|
+
if (focused === null || focused === document.body) throw new Error(`Key sequence "${keys}" was sent with nothing focused`);
|
|
576
|
+
await userEvent.keyboard(keys);
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
473
579
|
* Reaches a named control only through natural forward Tab traversal from the current focus.
|
|
474
580
|
*
|
|
475
581
|
* @param name - The target's exact accessible name.
|
|
@@ -608,6 +714,14 @@ function readValue(role, name) {
|
|
|
608
714
|
if (!(control instanceof HTMLInputElement) && !(control instanceof HTMLTextAreaElement) && !(control instanceof HTMLSelectElement)) throw new Error(`Interactive target "${name}" does not carry a value`);
|
|
609
715
|
return control.value;
|
|
610
716
|
}
|
|
717
|
+
function readRefusal(first, second) {
|
|
718
|
+
try {
|
|
719
|
+
resolveRendered(first, second);
|
|
720
|
+
} catch (thrown) {
|
|
721
|
+
if (isError(thrown)) return thrown.message;
|
|
722
|
+
throw thrown;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
611
725
|
/**
|
|
612
726
|
* Reads one element's rendered text the way a name computation reads it.
|
|
613
727
|
*
|
|
@@ -859,6 +973,119 @@ function describeFocus(element) {
|
|
|
859
973
|
function waitForFrame() {
|
|
860
974
|
return new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
|
861
975
|
}
|
|
976
|
+
async function waitForState(first, second, third, fourth) {
|
|
977
|
+
const keyed = isString(third);
|
|
978
|
+
const role = keyed ? first : void 0;
|
|
979
|
+
const name = keyed ? second : first;
|
|
980
|
+
const state = keyed ? third : second;
|
|
981
|
+
const options = keyed ? fourth : third;
|
|
982
|
+
const absent = options?.absent ?? false;
|
|
983
|
+
const description = `"${name}" to ${absent ? "stop announcing" : "announce"} "${state}"`;
|
|
984
|
+
let observed = [];
|
|
985
|
+
let readings = 0;
|
|
986
|
+
let refused;
|
|
987
|
+
try {
|
|
988
|
+
await waitForCondition(description, () => {
|
|
989
|
+
readings += 1;
|
|
990
|
+
try {
|
|
991
|
+
observed = readStates(role === void 0 ? resolveRendered(name) : resolveRendered(role, name));
|
|
992
|
+
} catch (thrown) {
|
|
993
|
+
refused = { thrown };
|
|
994
|
+
throw thrown;
|
|
995
|
+
}
|
|
996
|
+
return observed.includes(state) !== absent;
|
|
997
|
+
}, options);
|
|
998
|
+
} catch (cause) {
|
|
999
|
+
if (refused !== void 0 && cause === refused.thrown) throw cause;
|
|
1000
|
+
if (cause === options?.signal?.reason) throw cause;
|
|
1001
|
+
if (readings === 0) throw cause;
|
|
1002
|
+
if (!isError(cause)) throw cause;
|
|
1003
|
+
throw new Error(`${cause.message} (last states: ${JSON.stringify(observed)})`, { cause });
|
|
1004
|
+
}
|
|
1005
|
+
return observed;
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Waits until every finite animation on one element and its subtree has stopped moving.
|
|
1009
|
+
*
|
|
1010
|
+
* @param element - The element whose own animations and descendants' animations to wait on.
|
|
1011
|
+
* @param options - The time bounds and the abort signal.
|
|
1012
|
+
* @returns A promise resolving once no finite animation is still running.
|
|
1013
|
+
* @throws An `Error` when the element is not in a document, when a bound is invalid, or when an
|
|
1014
|
+
* animation is still running at the budget; or the abort reason.
|
|
1015
|
+
*
|
|
1016
|
+
* @remarks
|
|
1017
|
+
* A reading taken while paint is moving reports an interpolated frame — a `background-color` at a
|
|
1018
|
+
* fraction of its alpha, a `color` part way between two values — that no state of the interface
|
|
1019
|
+
* ever paints. This waits for the paint a person sees, whichever state it settles in.
|
|
1020
|
+
*
|
|
1021
|
+
* It parks on each animation's own `finished` promise rather than re-reading on a timer, and reads
|
|
1022
|
+
* the list again after each completion or cancellation, so an animation a finishing one starts is
|
|
1023
|
+
* waited on too.
|
|
1024
|
+
*
|
|
1025
|
+
* Some animations are left out, and each exclusion is a decision rather than an oversight. An animation
|
|
1026
|
+
* whose effect declares infinite iterations never finishes, so a spinner that runs forever is a
|
|
1027
|
+
* finding about the reading rather than a wait to lengthen. A finished animation filling its target
|
|
1028
|
+
* stays in the list a browser reports and is already at rest. A paused animation is at rest too,
|
|
1029
|
+
* and nothing here resumes it.
|
|
1030
|
+
*
|
|
1031
|
+
* The bounds are the wait family's, validated the same way. Default budget: `1000` milliseconds.
|
|
1032
|
+
* The interval is validated for consistency with the family and is not used, because this parks on
|
|
1033
|
+
* the animations. A detached element is refused rather than reported settled, because an element in
|
|
1034
|
+
* no document runs no animation and would answer `true` to every wait.
|
|
1035
|
+
*
|
|
1036
|
+
* @example
|
|
1037
|
+
* ```ts
|
|
1038
|
+
* await clickAccessible('Dark')
|
|
1039
|
+
* await waitForAnimations(document.body)
|
|
1040
|
+
* ```
|
|
1041
|
+
*/
|
|
1042
|
+
async function waitForAnimations(element, options) {
|
|
1043
|
+
const budget = options?.budget ?? 1e3;
|
|
1044
|
+
const interval = options?.interval ?? 10;
|
|
1045
|
+
checkBounds("Animation", budget, interval);
|
|
1046
|
+
if (!element.isConnected) throw new Error("Animation subject is not connected");
|
|
1047
|
+
const signal = options?.signal;
|
|
1048
|
+
const label = readName(element);
|
|
1049
|
+
const subject = `${readRole(element) ?? element.localName}${label.length > 0 ? ` "${label}"` : ""}`;
|
|
1050
|
+
const start = performance.now();
|
|
1051
|
+
let expired = false;
|
|
1052
|
+
let aborted;
|
|
1053
|
+
let timer;
|
|
1054
|
+
const expiry = new Promise((resolve) => {
|
|
1055
|
+
timer = setTimeout(() => {
|
|
1056
|
+
expired = true;
|
|
1057
|
+
resolve();
|
|
1058
|
+
}, budget);
|
|
1059
|
+
});
|
|
1060
|
+
try {
|
|
1061
|
+
while (true) {
|
|
1062
|
+
signal?.throwIfAborted();
|
|
1063
|
+
const running = element.getAnimations({ subtree: true }).filter((animation) => {
|
|
1064
|
+
const iterations = animation.effect?.getTiming().iterations ?? 1;
|
|
1065
|
+
return animation.playState === "running" && Number.isFinite(iterations);
|
|
1066
|
+
});
|
|
1067
|
+
if (running.length === 0) return;
|
|
1068
|
+
const elapsed = performance.now() - start;
|
|
1069
|
+
if (expired || elapsed >= budget) {
|
|
1070
|
+
const names = running.map((animation) => {
|
|
1071
|
+
if (animation instanceof CSSAnimation) return animation.animationName;
|
|
1072
|
+
if (animation instanceof CSSTransition) return animation.transitionProperty;
|
|
1073
|
+
return animation.id;
|
|
1074
|
+
});
|
|
1075
|
+
throw new Error(`Animation "${subject}" did not settle within ${budget}ms (waited ${elapsed}ms): ${names.join(", ")}`);
|
|
1076
|
+
}
|
|
1077
|
+
const pending = running.map((animation) => animation.finished.catch(() => void 0));
|
|
1078
|
+
pending.push(expiry);
|
|
1079
|
+
if (signal !== void 0) {
|
|
1080
|
+
aborted ??= waitForAbort(signal);
|
|
1081
|
+
pending.push(aborted);
|
|
1082
|
+
}
|
|
1083
|
+
await Promise.race(pending);
|
|
1084
|
+
}
|
|
1085
|
+
} finally {
|
|
1086
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
862
1089
|
/**
|
|
863
1090
|
* Builds one unmounted element of a known tag, wearing the classes, text, and attributes asked for.
|
|
864
1091
|
*
|
|
@@ -1423,6 +1650,42 @@ function readClasses(root) {
|
|
|
1423
1650
|
return authored;
|
|
1424
1651
|
}
|
|
1425
1652
|
/**
|
|
1653
|
+
* Takes the authored-class census of one subtree against the cascade this document loaded.
|
|
1654
|
+
*
|
|
1655
|
+
* @param root - The subtree to walk. A detached element and a `DocumentFragment` both work.
|
|
1656
|
+
* @returns The population walked, every class token the markup carries, and every one of them no
|
|
1657
|
+
* loaded stylesheet declares; both lists sorted.
|
|
1658
|
+
* @throws An `Error` when the walk reads no element at all.
|
|
1659
|
+
*
|
|
1660
|
+
* @remarks
|
|
1661
|
+
* This is {@link readClasses} differenced against {@link readCascade}, with the population reported
|
|
1662
|
+
* beside the difference. The population is what makes the reading falsifiable: an empty walk
|
|
1663
|
+
* reports no undeclared token, and so does a subtree whose every class the cascade declares, so a
|
|
1664
|
+
* check reading `undeclared` alone passes for a census that read nothing. The empty walk is refused
|
|
1665
|
+
* outright for the same reason.
|
|
1666
|
+
*
|
|
1667
|
+
* The root counts when it is an `Element`, so a `DocumentFragment` contributes its descendants
|
|
1668
|
+
* alone. Both lists are sorted rather than left in sighting order, because a census is compared
|
|
1669
|
+
* against a previous one or against an expected list, and document order is not a fact about the
|
|
1670
|
+
* classes.
|
|
1671
|
+
*
|
|
1672
|
+
* @example
|
|
1673
|
+
* ```ts
|
|
1674
|
+
* readCensus(container).undeclared // ['lead'] — no loaded stylesheet declares it
|
|
1675
|
+
* ```
|
|
1676
|
+
*/
|
|
1677
|
+
function readCensus(root) {
|
|
1678
|
+
const elements = (root instanceof Element ? 1 : 0) + root.querySelectorAll("*").length;
|
|
1679
|
+
if (elements === 0) throw new Error("Class census walked no element");
|
|
1680
|
+
const declared = readCascade();
|
|
1681
|
+
const tokens = [...readClasses(root)].sort();
|
|
1682
|
+
return Object.freeze({
|
|
1683
|
+
elements,
|
|
1684
|
+
tokens: Object.freeze(tokens),
|
|
1685
|
+
undeclared: Object.freeze(tokens.filter((token) => !declared.has(token)))
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
/**
|
|
1426
1689
|
* Collects every rule the stylesheets loaded into this document hold, nested grouping rules
|
|
1427
1690
|
* included.
|
|
1428
1691
|
*
|
|
@@ -2002,6 +2265,196 @@ function expandCaptures(states, variants) {
|
|
|
2002
2265
|
for (const state of states) for (const variant of variants) files.push(`${state}--${variant.name}.png`);
|
|
2003
2266
|
return files;
|
|
2004
2267
|
}
|
|
2268
|
+
/**
|
|
2269
|
+
* Builds the refusal a host withholding a storage operation raises.
|
|
2270
|
+
*
|
|
2271
|
+
* @param operation - The withheld operation, named as the `Storage` interface names it.
|
|
2272
|
+
* @param key - The storage key the operation addressed. Omit it for an operation that takes none.
|
|
2273
|
+
* @returns The refusal, unthrown.
|
|
2274
|
+
*
|
|
2275
|
+
* @remarks
|
|
2276
|
+
* A browser with site data blocked, a sandboxed frame, and a hardened privacy mode all raise a
|
|
2277
|
+
* `DOMException` named `SecurityError` from the storage object rather than answering, so this is
|
|
2278
|
+
* the voice rather than a message of this package's. {@link createStorage} raises it from every
|
|
2279
|
+
* operation the permission withholds; it is exported because a fixture implementing `Storage` some
|
|
2280
|
+
* other way needs the same voice rather than a second spelling of it.
|
|
2281
|
+
*
|
|
2282
|
+
* @example
|
|
2283
|
+
* ```ts
|
|
2284
|
+
* buildDenial('getItem', 'theme').message // 'Access is denied for getItem "theme"'
|
|
2285
|
+
* buildDenial('length').name // 'SecurityError'
|
|
2286
|
+
* ```
|
|
2287
|
+
*/
|
|
2288
|
+
function buildDenial(operation, key) {
|
|
2289
|
+
return new DOMException(`Access is denied for ${operation}${key === void 0 ? "" : ` "${key}"`}`, "SecurityError");
|
|
2290
|
+
}
|
|
2291
|
+
/**
|
|
2292
|
+
* Builds a detached translucent stack whose composited and flat contrast readings straddle one bar.
|
|
2293
|
+
*
|
|
2294
|
+
* @param bar - The contrast ratio `refused` and `accepted` must sit on opposite sides of.
|
|
2295
|
+
* @returns The opaque root carrying the tint, and the refused and accepted foregrounds under it.
|
|
2296
|
+
* @throws An `Error` when no grey foreground puts the two readings on opposite sides of the bar.
|
|
2297
|
+
*
|
|
2298
|
+
* @remarks
|
|
2299
|
+
* A contrast instrument that never composites still clears every fixture painting its own opaque
|
|
2300
|
+
* background, so this is the control that makes {@link readContrast}'s ancestor walk and alpha
|
|
2301
|
+
* blend the thing under test. The stack is an opaque floor, a translucent tint over it, and two
|
|
2302
|
+
* grey foregrounds inside the tint. `refused` reads under the bar composited and at or over it
|
|
2303
|
+
* flat, and `accepted` reads the other way about, so no single non-compositing reading satisfies
|
|
2304
|
+
* both.
|
|
2305
|
+
*
|
|
2306
|
+
* The greys are searched rather than written down, so the control follows the bar it was asked for.
|
|
2307
|
+
* A bar at or under `1` is refused because every contrast ratio reaches `1`, and a bar above what
|
|
2308
|
+
* the tinted surface can reach is refused because no foreground clears it — each refusal names the
|
|
2309
|
+
* bar rather than returning a stack that proves nothing.
|
|
2310
|
+
*
|
|
2311
|
+
* The nodes are detached, so nothing is mounted for you. Append `root` to the surface you are
|
|
2312
|
+
* reading, take both readings, and remove it: a computed color needs the document, and a fixture
|
|
2313
|
+
* left behind is the next test's resolver ambiguity.
|
|
2314
|
+
*
|
|
2315
|
+
* @example
|
|
2316
|
+
* ```ts
|
|
2317
|
+
* const control = buildContrast(4.5)
|
|
2318
|
+
* mount(control.root)
|
|
2319
|
+
* readContrast(control.refused) < 4.5 // true
|
|
2320
|
+
* readContrast(control.accepted) >= 4.5 // true
|
|
2321
|
+
* control.root.remove()
|
|
2322
|
+
* ```
|
|
2323
|
+
*/
|
|
2324
|
+
function buildContrast(bar) {
|
|
2325
|
+
const tint = [
|
|
2326
|
+
0,
|
|
2327
|
+
0,
|
|
2328
|
+
0,
|
|
2329
|
+
.06
|
|
2330
|
+
];
|
|
2331
|
+
const backdrop = blendColor(tint, CANVAS_COLOR);
|
|
2332
|
+
const flattened = [
|
|
2333
|
+
tint[0],
|
|
2334
|
+
tint[1],
|
|
2335
|
+
tint[2],
|
|
2336
|
+
1
|
|
2337
|
+
];
|
|
2338
|
+
let refusedChannel;
|
|
2339
|
+
let acceptedChannel;
|
|
2340
|
+
for (let channel = 0; channel <= 255; channel += 1) {
|
|
2341
|
+
const front = [
|
|
2342
|
+
channel,
|
|
2343
|
+
channel,
|
|
2344
|
+
channel,
|
|
2345
|
+
1
|
|
2346
|
+
];
|
|
2347
|
+
const composited = measureContrast(front, backdrop);
|
|
2348
|
+
const flat = measureContrast(front, flattened);
|
|
2349
|
+
if (refusedChannel === void 0 && composited < bar && flat >= bar) refusedChannel = channel;
|
|
2350
|
+
if (acceptedChannel === void 0 && composited >= bar && flat < bar) acceptedChannel = channel;
|
|
2351
|
+
}
|
|
2352
|
+
if (refusedChannel === void 0 || acceptedChannel === void 0) throw new Error(`Contrast control cannot straddle the bar ${bar}`);
|
|
2353
|
+
const root = build("div", { attributes: { style: `background-color: rgb(${CANVAS_COLOR[0]}, ${CANVAS_COLOR[1]}, ${CANVAS_COLOR[2]})` } });
|
|
2354
|
+
const tinted = build("div", { attributes: { style: `background-color: rgba(${tint[0]}, ${tint[1]}, ${tint[2]}, ${tint[3]})` } });
|
|
2355
|
+
const refused = build("p", {
|
|
2356
|
+
text: "Composited contrast control",
|
|
2357
|
+
attributes: { style: `color: rgb(${refusedChannel}, ${refusedChannel}, ${refusedChannel})` }
|
|
2358
|
+
});
|
|
2359
|
+
const accepted = build("p", {
|
|
2360
|
+
text: "Composited contrast survivor",
|
|
2361
|
+
attributes: { style: `color: rgb(${acceptedChannel}, ${acceptedChannel}, ${acceptedChannel})` }
|
|
2362
|
+
});
|
|
2363
|
+
tinted.append(refused, accepted);
|
|
2364
|
+
root.append(tinted);
|
|
2365
|
+
return Object.freeze({
|
|
2366
|
+
root,
|
|
2367
|
+
refused,
|
|
2368
|
+
accepted
|
|
2369
|
+
});
|
|
2370
|
+
}
|
|
2371
|
+
/**
|
|
2372
|
+
* Builds detached markup carrying one style escape of each kind, plus the sheet a project allows.
|
|
2373
|
+
*
|
|
2374
|
+
* @param permitted - The `id` the caller's reading exempts, placed on the third element.
|
|
2375
|
+
* @returns The detached root, the inline escape, the embedded escape, and the exempt sheet.
|
|
2376
|
+
*
|
|
2377
|
+
* @remarks
|
|
2378
|
+
* {@link extractStyles} has two branches — an inline `style` attribute and a `<style>` element —
|
|
2379
|
+
* and a reading fed only the first never exercises the second. The exempt sheet is the other half
|
|
2380
|
+
* of the control: a project that allows one standalone stylesheet writes an exemption for its id,
|
|
2381
|
+
* and a reading that passes by refusing every `<style>` element clears the two escapes and fails
|
|
2382
|
+
* that exemption.
|
|
2383
|
+
*
|
|
2384
|
+
* The declaration `inline`, `embedded`, and `permitted` carry is this package's, so assert on which
|
|
2385
|
+
* elements are reported rather than on what they declare.
|
|
2386
|
+
*
|
|
2387
|
+
* The nodes are detached and stay that way: `extractStyles` takes any `ParentNode`, so the reading
|
|
2388
|
+
* runs without mounting, and an embedded sheet that reached the document would join the cascade
|
|
2389
|
+
* every other reading measures against.
|
|
2390
|
+
*
|
|
2391
|
+
* @example
|
|
2392
|
+
* ```ts
|
|
2393
|
+
* const control = buildEscapes('project-stylesheet')
|
|
2394
|
+
* extractStyles(control.root).length // 3 — the inline escape, the embedded sheet, and the exempt one
|
|
2395
|
+
* ```
|
|
2396
|
+
*/
|
|
2397
|
+
function buildEscapes(permitted) {
|
|
2398
|
+
const declaration = "color: rgb(1, 2, 3)";
|
|
2399
|
+
const root = build("div");
|
|
2400
|
+
const inline = build("p", {
|
|
2401
|
+
text: "Inline escape control",
|
|
2402
|
+
attributes: { style: declaration }
|
|
2403
|
+
});
|
|
2404
|
+
const embedded = build("style");
|
|
2405
|
+
embedded.textContent = `.escape-embedded { ${declaration} }`;
|
|
2406
|
+
const exempt = build("style", { attributes: { id: permitted } });
|
|
2407
|
+
exempt.textContent = `#escape-permitted { ${declaration} }`;
|
|
2408
|
+
root.append(inline, embedded, exempt);
|
|
2409
|
+
return Object.freeze({
|
|
2410
|
+
root,
|
|
2411
|
+
inline,
|
|
2412
|
+
embedded,
|
|
2413
|
+
permitted: exempt
|
|
2414
|
+
});
|
|
2415
|
+
}
|
|
2416
|
+
/**
|
|
2417
|
+
* Builds detached markup carrying one undeclared class token on HTML and another on SVG.
|
|
2418
|
+
*
|
|
2419
|
+
* @returns The detached root, the token the HTML element carries, and the one the SVG carries.
|
|
2420
|
+
*
|
|
2421
|
+
* @remarks
|
|
2422
|
+
* The SVG element is the trap a census has to survive: `className` on an SVG element is an
|
|
2423
|
+
* `SVGAnimatedString` rather than a string, so a reader splitting that value finds nothing and
|
|
2424
|
+
* reports one undeclared token where two are carried. {@link readCensus} reads every element
|
|
2425
|
+
* through `classList`, and this is the control that proves it.
|
|
2426
|
+
*
|
|
2427
|
+
* The two tokens are returned rather than written into a caller's expectation, so a cascade that
|
|
2428
|
+
* later declares one of these names moves the fixture and the assertion together. Each token also
|
|
2429
|
+
* carries a suffix drawn per call from `crypto.getRandomValues`, so a consumer cascade cannot
|
|
2430
|
+
* declare either of them in advance and two controls in one document never share a token.
|
|
2431
|
+
* `getRandomValues` rather than `randomUUID`, because that one answers outside a secure context
|
|
2432
|
+
* too, and a browser project served from a remote host is not one.
|
|
2433
|
+
*
|
|
2434
|
+
* @example
|
|
2435
|
+
* ```ts
|
|
2436
|
+
* const control = buildCensus()
|
|
2437
|
+
* readCensus(control.root).undeclared // [control.mark, control.token], sorted
|
|
2438
|
+
* ```
|
|
2439
|
+
*/
|
|
2440
|
+
function buildCensus() {
|
|
2441
|
+
const suffix = crypto.getRandomValues(/* @__PURE__ */ new Uint32Array(1)).join("");
|
|
2442
|
+
const token = `census-authored-token-${suffix}`;
|
|
2443
|
+
const mark = `census-authored-mark-${suffix}`;
|
|
2444
|
+
const root = build("div");
|
|
2445
|
+
root.append(build("p", {
|
|
2446
|
+
classes: token,
|
|
2447
|
+
text: "Authored class control"
|
|
2448
|
+
}));
|
|
2449
|
+
const glyph = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
2450
|
+
glyph.setAttribute("class", mark);
|
|
2451
|
+
root.append(glyph);
|
|
2452
|
+
return Object.freeze({
|
|
2453
|
+
root,
|
|
2454
|
+
token,
|
|
2455
|
+
mark
|
|
2456
|
+
});
|
|
2457
|
+
}
|
|
2005
2458
|
//#endregion
|
|
2006
2459
|
//#region src/browser/factories.ts
|
|
2007
2460
|
/**
|
|
@@ -2245,7 +2698,239 @@ function createJournal() {
|
|
|
2245
2698
|
}
|
|
2246
2699
|
};
|
|
2247
2700
|
}
|
|
2701
|
+
/**
|
|
2702
|
+
* Creates an inert `Storage` a host can withhold, grant, and run out of room in.
|
|
2703
|
+
*
|
|
2704
|
+
* @param options - The seed, the read and write permissions, and the quota.
|
|
2705
|
+
* @returns A store carrying the Web Storage surface plus the grant.
|
|
2706
|
+
* @throws An `Error` when `quota` is not a non-negative safe integer. A quota above
|
|
2707
|
+
* `Number.MAX_SAFE_INTEGER` carries the same sentence, because a counter that cannot decrement past
|
|
2708
|
+
* that point bounds nothing.
|
|
2709
|
+
*
|
|
2710
|
+
* @remarks
|
|
2711
|
+
* Conditions a real origin produces are unreachable from a test otherwise: a browser with
|
|
2712
|
+
* site data blocked refuses every operation the permission withholds, an origin with no room left
|
|
2713
|
+
* refuses `setItem`, and a person allowing site data grants what was withheld. This makes each of
|
|
2714
|
+
* them reachable against a real `Storage` surface rather than a shaped object.
|
|
2715
|
+
*
|
|
2716
|
+
* The store answers through its methods and intercepts no named-property access, so drive a
|
|
2717
|
+
* consumer under test through `getItem` and `setItem`. `Storage` declares an index signature, so
|
|
2718
|
+
* `store.theme` typechecks and reads `undefined` while `getItem('theme')` answers, and a property
|
|
2719
|
+
* write lands on the object rather than in the store, consuming no quota and meeting no refusal.
|
|
2720
|
+
*
|
|
2721
|
+
* It is backed by a map of its own and patches nothing: `localStorage` and `sessionStorage` are
|
|
2722
|
+
* untouched, no `storage` event is dispatched, and the store is reached only by the code the test
|
|
2723
|
+
* hands it to. Reach for {@link clearStorage} where the real browser surfaces are the subject.
|
|
2724
|
+
*
|
|
2725
|
+
* `length`, `key`, and `getItem` are reads; `clear`, `removeItem`, and `setItem` are writes. A
|
|
2726
|
+
* withheld operation raises {@link buildDenial}'s `SecurityError`, and `permit` lifts both
|
|
2727
|
+
* permissions at once, the way a person allowing site data lifts them. Reads answer from what the
|
|
2728
|
+
* store actually accepted, so a journey reads what the application kept while the host was refusing.
|
|
2729
|
+
*
|
|
2730
|
+
* `quota` counts accepted `setItem` calls rather than bytes, because the number of writes is what a
|
|
2731
|
+
* journey scripts and a byte budget is the browser's own arithmetic. `removeItem` consumes none of
|
|
2732
|
+
* it, and `permit` replenishes none of it: room and permission are different refusals, and a test
|
|
2733
|
+
* that granted the permission still meets the full origin.
|
|
2734
|
+
*
|
|
2735
|
+
* @example
|
|
2736
|
+
* ```ts
|
|
2737
|
+
* const storage = createStorage({ values: { theme: 'dark' }, writes: false })
|
|
2738
|
+
* storage.getItem('theme') // 'dark'
|
|
2739
|
+
* storage.permit()
|
|
2740
|
+
* storage.setItem('theme', 'light')
|
|
2741
|
+
* ```
|
|
2742
|
+
*/
|
|
2743
|
+
function createStorage(options) {
|
|
2744
|
+
const quota = options?.quota;
|
|
2745
|
+
if (quota !== void 0 && (!Number.isSafeInteger(quota) || quota < 0)) throw new Error("Storage quota must be a non-negative integer");
|
|
2746
|
+
const values = new Map(Object.entries(options?.values ?? {}));
|
|
2747
|
+
let reads = options?.reads ?? true;
|
|
2748
|
+
let writes = options?.writes ?? true;
|
|
2749
|
+
let room = quota;
|
|
2750
|
+
return {
|
|
2751
|
+
get length() {
|
|
2752
|
+
if (!reads) throw buildDenial("length");
|
|
2753
|
+
return values.size;
|
|
2754
|
+
},
|
|
2755
|
+
permit() {
|
|
2756
|
+
reads = true;
|
|
2757
|
+
writes = true;
|
|
2758
|
+
},
|
|
2759
|
+
clear() {
|
|
2760
|
+
if (!writes) throw buildDenial("clear");
|
|
2761
|
+
values.clear();
|
|
2762
|
+
},
|
|
2763
|
+
getItem(key) {
|
|
2764
|
+
if (!reads) throw buildDenial("getItem", key);
|
|
2765
|
+
return values.get(key) ?? null;
|
|
2766
|
+
},
|
|
2767
|
+
key(index) {
|
|
2768
|
+
if (!reads) throw buildDenial("key");
|
|
2769
|
+
return [...values.keys()][index] ?? null;
|
|
2770
|
+
},
|
|
2771
|
+
removeItem(key) {
|
|
2772
|
+
if (!writes) throw buildDenial("removeItem", key);
|
|
2773
|
+
values.delete(key);
|
|
2774
|
+
},
|
|
2775
|
+
setItem(key, value) {
|
|
2776
|
+
if (!writes) throw buildDenial("setItem", key);
|
|
2777
|
+
if (room !== void 0) {
|
|
2778
|
+
if (room === 0) throw new DOMException(`No room is left for ${key}`, "QuotaExceededError");
|
|
2779
|
+
room -= 1;
|
|
2780
|
+
}
|
|
2781
|
+
values.set(key, value);
|
|
2782
|
+
}
|
|
2783
|
+
};
|
|
2784
|
+
}
|
|
2785
|
+
/**
|
|
2786
|
+
* Creates a mounted statechart harness that renders one transition table and drives it row by row.
|
|
2787
|
+
*
|
|
2788
|
+
* @typeParam TState - The states the entity moves between.
|
|
2789
|
+
* @typeParam TEvent - The events the entity accepts.
|
|
2790
|
+
* @typeParam TContext - The fixture each row drives.
|
|
2791
|
+
* @param options - The table, the fixture builder, the state reader, and the delay between rows.
|
|
2792
|
+
* @returns The mounted harness, standing idle with its tally at zero.
|
|
2793
|
+
* @throws An `Error` reading `Statechart harness mounted no transition` for an empty table, before
|
|
2794
|
+
* anything reaches the document.
|
|
2795
|
+
*
|
|
2796
|
+
* @remarks
|
|
2797
|
+
* A page cannot import this package, because the browser entry imports `vitest/browser` at module
|
|
2798
|
+
* scope. So the harness is test-side: the suite mounts it, a gate outside the page polls the
|
|
2799
|
+
* markup it renders, and `STATECHART_ATTRIBUTES` is the whole contract between the two. Nothing
|
|
2800
|
+
* here spells a `data-statechart-*` string of its own, and neither does a gate. That gate has no
|
|
2801
|
+
* rejection channel, so every exit writes a terminal status: a run that completes writes `passed` or
|
|
2802
|
+
* `failed`, and a run that a `state` reader or a non-`Error` phase throw ends writes `failed` and
|
|
2803
|
+
* then rejects with that value by identity, without counting the row as failed.
|
|
2804
|
+
*
|
|
2805
|
+
* The markup is framework-free. The root carries `status` and the tally; a `role="status"`
|
|
2806
|
+
* announcer narrates each step in a sentence; one element carries `state` and renders what the
|
|
2807
|
+
* entity's own reader reports; and an ordered list carries one row per scenario, each labelled with
|
|
2808
|
+
* its `from`, its `event`, and its `to` and marked with the transition's name. The `state` element
|
|
2809
|
+
* mounts empty and takes its attribute from the first row that produces a context, because a state
|
|
2810
|
+
* is read from an entity and no entity exists until a row builds one.
|
|
2811
|
+
*
|
|
2812
|
+
* Construction writes `pending`, mounts the root, renders every row, then writes the row count and
|
|
2813
|
+
* `idle` — so a gate that reads `pending` has found a harness whose rows never mounted, and the
|
|
2814
|
+
* order is observable from outside through the mutations the document records.
|
|
2815
|
+
*
|
|
2816
|
+
* `execute` clears every rendered result and the rendered state, writes `running`, and drives each
|
|
2817
|
+
* row in order through {@link executeScenario} against a context of that row's own. It continues
|
|
2818
|
+
* past a failing row, so one run reports on the whole table rather than stopping at the first
|
|
2819
|
+
* finding, and a builder that throws counts as its row failing under {@link buildRefusal}'s
|
|
2820
|
+
* sentence, which is the one `executeScenarios` raises. What decides whether a row's phases run is
|
|
2821
|
+
* whether its builder returned, not what it returned, so a table whose context is `undefined` drives
|
|
2822
|
+
* every phase. A second `execute` runs the same table from a fresh tally and a cleared state.
|
|
2823
|
+
*
|
|
2824
|
+
* Every reading comes off the markup, so the object and the page cannot disagree, and `failures` is
|
|
2825
|
+
* the `scenario` name of each row whose rendered `result` reads `failed` rather than a second list
|
|
2826
|
+
* beside them.
|
|
2827
|
+
*
|
|
2828
|
+
* @example
|
|
2829
|
+
* ```ts
|
|
2830
|
+
* const harness = createHarness({ scenarios: SCENARIOS, build: buildDisclosure, state: readState })
|
|
2831
|
+
* await harness.execute()
|
|
2832
|
+
* harness.status // 'passed'
|
|
2833
|
+
* harness.destroy()
|
|
2834
|
+
* ```
|
|
2835
|
+
*/
|
|
2836
|
+
function createHarness(options) {
|
|
2837
|
+
const scenarios = options.scenarios;
|
|
2838
|
+
if (scenarios.length === 0) throw new Error("Statechart harness mounted no transition");
|
|
2839
|
+
const root = mount(build("div", { attributes: { [STATECHART_ATTRIBUTES.status]: "pending" } }));
|
|
2840
|
+
const announcer = build("p", { attributes: { role: "status" } });
|
|
2841
|
+
const state = build("p");
|
|
2842
|
+
const rows = build("ol");
|
|
2843
|
+
root.append(announcer, state, rows);
|
|
2844
|
+
const table = scenarios.map((scenario) => ({
|
|
2845
|
+
scenario,
|
|
2846
|
+
element: build("li", {
|
|
2847
|
+
text: `${scenario.transition.name}: ${scenario.transition.from} on ${scenario.transition.event} becomes ${scenario.transition.to}`,
|
|
2848
|
+
attributes: { [STATECHART_ATTRIBUTES.scenario]: scenario.transition.name }
|
|
2849
|
+
})
|
|
2850
|
+
}));
|
|
2851
|
+
for (const row of table) rows.append(row.element);
|
|
2852
|
+
root.setAttribute(STATECHART_ATTRIBUTES.total, String(table.length));
|
|
2853
|
+
root.setAttribute(STATECHART_ATTRIBUTES.passed, "0");
|
|
2854
|
+
root.setAttribute(STATECHART_ATTRIBUTES.failed, "0");
|
|
2855
|
+
root.setAttribute(STATECHART_ATTRIBUTES.status, "idle");
|
|
2856
|
+
announcer.textContent = `Statechart harness is idle, 0 passed and 0 failed of ${table.length}.`;
|
|
2857
|
+
return {
|
|
2858
|
+
root,
|
|
2859
|
+
get status() {
|
|
2860
|
+
const written = root.getAttribute(STATECHART_ATTRIBUTES.status);
|
|
2861
|
+
return requireValue(STATECHART_STATUSES.find((member) => member === written), "Statechart harness carries no status");
|
|
2862
|
+
},
|
|
2863
|
+
get total() {
|
|
2864
|
+
return Number(root.getAttribute(STATECHART_ATTRIBUTES.total));
|
|
2865
|
+
},
|
|
2866
|
+
get passed() {
|
|
2867
|
+
return Number(root.getAttribute(STATECHART_ATTRIBUTES.passed));
|
|
2868
|
+
},
|
|
2869
|
+
get failed() {
|
|
2870
|
+
return Number(root.getAttribute(STATECHART_ATTRIBUTES.failed));
|
|
2871
|
+
},
|
|
2872
|
+
get failures() {
|
|
2873
|
+
const names = [];
|
|
2874
|
+
for (const row of table) {
|
|
2875
|
+
if (row.element.getAttribute(STATECHART_ATTRIBUTES.result) !== "failed") continue;
|
|
2876
|
+
const name = row.element.getAttribute(STATECHART_ATTRIBUTES.scenario);
|
|
2877
|
+
if (name !== null) names.push(name);
|
|
2878
|
+
}
|
|
2879
|
+
return names;
|
|
2880
|
+
},
|
|
2881
|
+
async execute() {
|
|
2882
|
+
for (const row of table) row.element.removeAttribute(STATECHART_ATTRIBUTES.result);
|
|
2883
|
+
state.removeAttribute(STATECHART_ATTRIBUTES.state);
|
|
2884
|
+
state.textContent = "";
|
|
2885
|
+
let passed = 0;
|
|
2886
|
+
let failed = 0;
|
|
2887
|
+
root.setAttribute(STATECHART_ATTRIBUTES.passed, "0");
|
|
2888
|
+
root.setAttribute(STATECHART_ATTRIBUTES.failed, "0");
|
|
2889
|
+
root.setAttribute(STATECHART_ATTRIBUTES.status, "running");
|
|
2890
|
+
announcer.textContent = `Statechart harness is running, 0 passed and 0 failed of ${table.length}.`;
|
|
2891
|
+
try {
|
|
2892
|
+
for (const [index, row] of table.entries()) {
|
|
2893
|
+
let built;
|
|
2894
|
+
let refusal;
|
|
2895
|
+
try {
|
|
2896
|
+
built = { context: await options.build(row.scenario) };
|
|
2897
|
+
} catch (cause) {
|
|
2898
|
+
refusal = buildRefusal(row.scenario.transition.name, cause).message;
|
|
2899
|
+
}
|
|
2900
|
+
if (built !== void 0) {
|
|
2901
|
+
try {
|
|
2902
|
+
await executeScenario(row.scenario, built.context);
|
|
2903
|
+
} catch (cause) {
|
|
2904
|
+
if (!isError(cause)) throw cause;
|
|
2905
|
+
refusal = cause.message;
|
|
2906
|
+
}
|
|
2907
|
+
const current = options.state(built.context);
|
|
2908
|
+
state.setAttribute(STATECHART_ATTRIBUTES.state, current);
|
|
2909
|
+
state.textContent = current;
|
|
2910
|
+
}
|
|
2911
|
+
if (refusal === void 0) passed += 1;
|
|
2912
|
+
else failed += 1;
|
|
2913
|
+
row.element.setAttribute(STATECHART_ATTRIBUTES.result, refusal === void 0 ? "passed" : "failed");
|
|
2914
|
+
root.setAttribute(STATECHART_ATTRIBUTES.passed, String(passed));
|
|
2915
|
+
root.setAttribute(STATECHART_ATTRIBUTES.failed, String(failed));
|
|
2916
|
+
announcer.textContent = refusal ?? `${row.scenario.transition.name} passed.`;
|
|
2917
|
+
if (options.pause !== void 0 && index < table.length - 1) await waitForDelay(options.pause);
|
|
2918
|
+
}
|
|
2919
|
+
} catch (cause) {
|
|
2920
|
+
root.setAttribute(STATECHART_ATTRIBUTES.status, "failed");
|
|
2921
|
+
announcer.textContent = `Statechart harness failed, ${passed} passed and ${failed} failed of ${table.length}.`;
|
|
2922
|
+
throw cause;
|
|
2923
|
+
}
|
|
2924
|
+
const outcome = failed === 0 ? "passed" : "failed";
|
|
2925
|
+
root.setAttribute(STATECHART_ATTRIBUTES.status, outcome);
|
|
2926
|
+
announcer.textContent = `Statechart harness ${outcome}, ${passed} passed and ${failed} failed of ${table.length}.`;
|
|
2927
|
+
},
|
|
2928
|
+
destroy() {
|
|
2929
|
+
root.remove();
|
|
2930
|
+
}
|
|
2931
|
+
};
|
|
2932
|
+
}
|
|
2248
2933
|
//#endregion
|
|
2249
|
-
export { ACCESSIBLE_ROLES, CANVAS_COLOR, CAPTURE_PANE, CAPTURE_STAGINGS, CONTENT_ROLES, FIELD_ROLES, FOCUSABLE_SELECTOR, HEADER_ROLES, IMPLICIT_ROLES, blendColor, build, captureFrame, clearStorage, clickAccessible, clickAccessibleWithin, clickDisclosure, commitInput, computeNamePattern, createChannel, createDragEvent, createJournal, createPointerEvent, createPortfolio, describeFocus, describeTree, expandCaptures, extractOrphans, extractStyles, fillAccessible, findKeyframes, findRule, isOutsideViewport, isReachable, isRendered, matchesColor, measureContent, measureContrast, measureLuminance, mount, parseCSSColor, parseColor, readBackdrop, readCascade, readClasses, readContrast, readFocus, readFrame, readLayers, readName, readPage, readPerception, readPixels, readRing, readRole, readRootToken, readRows, readRules, readStates, readStyle, readText, readToken, readValue, releasePane, removeDatabase, render, resolveAccessible, resolveRendered, stagePane, traverseAccessible, typeAccessible, typeInput, waitForFrame };
|
|
2934
|
+
export { ACCESSIBLE_ROLES, CANVAS_COLOR, CAPTURE_PANE, CAPTURE_STAGINGS, CONTENT_ROLES, FIELD_ROLES, FOCUSABLE_SELECTOR, HEADER_ROLES, IMPLICIT_ROLES, blendColor, build, buildCensus, buildContrast, buildDenial, buildEscapes, captureFrame, clearStorage, clickAccessible, clickAccessibleWithin, clickDisclosure, commitInput, computeNamePattern, createChannel, createDragEvent, createHarness, createJournal, createPointerEvent, createPortfolio, createStorage, describeFocus, describeTree, expandCaptures, extractOrphans, extractStyles, fillAccessible, findKeyframes, findRule, isOutsideViewport, isReachable, isRendered, matchesColor, measureContent, measureContrast, measureLuminance, mount, parseCSSColor, parseColor, pressKeys, readBackdrop, readCascade, readCensus, readClasses, readContrast, readFocus, readFrame, readHit, readLayers, readName, readPage, readPerception, readPixels, readRefusal, readRing, readRole, readRootToken, readRows, readRules, readStates, readStyle, readText, readToken, readValue, releasePane, removeDatabase, render, resolveAccessible, resolveRendered, stagePane, traverseAccessible, typeAccessible, typeInput, waitForAnimations, waitForFrame, waitForState };
|
|
2250
2935
|
|
|
2251
2936
|
//# sourceMappingURL=index.js.map
|