@orkestrel/test 0.0.8 → 0.0.9
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/src/browser/index.d.ts +452 -6
- package/dist/src/browser/index.js +489 -24
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +288 -8
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +180 -5
- package/dist/src/core/index.d.ts +180 -5
- package/dist/src/core/index.js +281 -9
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +242 -1
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +139 -0
- package/dist/src/server/index.d.ts +139 -0
- package/dist/src/server/index.js +238 -3
- package/dist/src/server/index.js.map +1 -1
- package/package.json +6 -6
|
@@ -799,22 +799,119 @@ function waitForFrame() {
|
|
|
799
799
|
return new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
|
800
800
|
}
|
|
801
801
|
/**
|
|
802
|
-
*
|
|
802
|
+
* Builds one unmounted element of a known tag, wearing the classes, text, and attributes asked for.
|
|
803
803
|
*
|
|
804
|
-
* @param
|
|
805
|
-
* @
|
|
804
|
+
* @param tag - The HTML tag name, which fixes the returned element's exact type.
|
|
805
|
+
* @param options - The class list, the text, and the attributes to apply.
|
|
806
|
+
* @returns The built element, not yet in any document.
|
|
807
|
+
*
|
|
808
|
+
* @remarks
|
|
809
|
+
* The element is unmounted on purpose, so a fixture is assembled before the page ever sees it and a
|
|
810
|
+
* test decides where it goes. Nothing here resolves against the cascade: a built element computes no
|
|
811
|
+
* style and lays out no box until {@link mount} puts it in the document.
|
|
812
|
+
*
|
|
813
|
+
* The text is set as text rather than parsed as markup, so a `<` in it stays a `<`. Use
|
|
814
|
+
* {@link render} where the fixture is markup.
|
|
815
|
+
*
|
|
816
|
+
* @example
|
|
817
|
+
* ```ts
|
|
818
|
+
* const button = build('button', { classes: 'primary', text: 'Save', attributes: { type: 'button' } })
|
|
819
|
+
* ```
|
|
820
|
+
*/
|
|
821
|
+
function build(tag, options) {
|
|
822
|
+
const element = document.createElement(tag);
|
|
823
|
+
if (options?.classes !== void 0) element.className = options.classes;
|
|
824
|
+
if (options?.text !== void 0) element.textContent = options.text;
|
|
825
|
+
for (const [name, value] of Object.entries(options?.attributes ?? {})) element.setAttribute(name, value);
|
|
826
|
+
return element;
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Puts one element into the document and hands it straight back.
|
|
830
|
+
*
|
|
831
|
+
* @param element - The element to attach.
|
|
832
|
+
* @returns The same element, now appended to `document.body`.
|
|
833
|
+
*
|
|
834
|
+
* @remarks
|
|
835
|
+
* What this buys is the composition, not the attachment: the `append` method returns `void`, and
|
|
836
|
+
* this hands the element back, so it fits where an expression is expected. The {@link render} helper
|
|
837
|
+
* returns its fixture through it, and the {@link rgba} helper probes through `mount(build('span'))`.
|
|
838
|
+
* A bare `append` call breaks each of those call sites.
|
|
839
|
+
*
|
|
840
|
+
* Being connected is what the attachment then buys: `getComputedStyle` resolves against the shipped
|
|
841
|
+
* cascade, custom properties inherit from `:root`, and the element lays out a real box. A detached
|
|
842
|
+
* element answers each of those questions with the initial value instead, which reads as a styling
|
|
843
|
+
* defect rather than as a detached node.
|
|
844
|
+
*
|
|
845
|
+
* Taking it back out belongs to the consumer's teardown, because this records nothing: a browser
|
|
846
|
+
* test file shares one page, so a fixture left behind is the next test's resolver ambiguity. Build a
|
|
847
|
+
* recorded container in a setup module and remove it from an `afterEach` hook.
|
|
848
|
+
*
|
|
849
|
+
* @example
|
|
850
|
+
* ```ts
|
|
851
|
+
* const panel = mount(build('div', { classes: 'surface' }))
|
|
852
|
+
* panel.remove()
|
|
853
|
+
* ```
|
|
854
|
+
*/
|
|
855
|
+
function mount(element) {
|
|
856
|
+
document.body.append(element);
|
|
857
|
+
return element;
|
|
858
|
+
}
|
|
859
|
+
function render(first, second) {
|
|
860
|
+
if (second === void 0) {
|
|
861
|
+
const container = build("div");
|
|
862
|
+
container.innerHTML = first;
|
|
863
|
+
return mount(container);
|
|
864
|
+
}
|
|
865
|
+
const element = document.createElement(first);
|
|
866
|
+
element.className = second;
|
|
867
|
+
return mount(element);
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Sets one field's value and announces it the way typing into the field does.
|
|
871
|
+
*
|
|
872
|
+
* @param element - The input or textarea to write into.
|
|
873
|
+
* @param text - The value to set.
|
|
874
|
+
*
|
|
875
|
+
* @remarks
|
|
876
|
+
* This is the synthetic pair of {@link typeAccessible}, for a component that listens for `input` and
|
|
877
|
+
* a test that has the element already. It sets the value in one write and dispatches one bubbling
|
|
878
|
+
* `input` event, so a delegated listener on an ancestor hears it. It sends no keystrokes, so a
|
|
879
|
+
* component reading `key`, composition, or selection sees nothing. The dispatched event is a plain
|
|
880
|
+
* `Event`, never an `InputEvent`, so a component reading `inputType` or testing
|
|
881
|
+
* `instanceof InputEvent` sees neither. Drive a component that reads any of those through
|
|
882
|
+
* `typeAccessible` instead.
|
|
883
|
+
*
|
|
884
|
+
* No `change` event follows. Use {@link commitInput} where the component waits for the field to be
|
|
885
|
+
* committed.
|
|
886
|
+
*
|
|
887
|
+
* @example
|
|
888
|
+
* ```ts
|
|
889
|
+
* typeInput(requireValue(container.querySelector('input')), 'Ada')
|
|
890
|
+
* ```
|
|
891
|
+
*/
|
|
892
|
+
function typeInput(element, text) {
|
|
893
|
+
element.value = text;
|
|
894
|
+
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Sets one field's value and commits it, the way typing and then leaving the field does.
|
|
898
|
+
*
|
|
899
|
+
* @param element - The input or textarea to write into.
|
|
900
|
+
* @param text - The value to set.
|
|
901
|
+
*
|
|
902
|
+
* @remarks
|
|
903
|
+
* The order is the browser's: {@link typeInput} first, so `input` is dispatched with the value
|
|
904
|
+
* already set, and one bubbling `change` after it. A component that reads the value from either
|
|
905
|
+
* event therefore reads `text` from both.
|
|
806
906
|
*
|
|
807
907
|
* @example
|
|
808
908
|
* ```ts
|
|
809
|
-
*
|
|
810
|
-
* container.remove()
|
|
909
|
+
* commitInput(requireValue(container.querySelector('input')), 'Ada')
|
|
811
910
|
* ```
|
|
812
911
|
*/
|
|
813
|
-
function
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
document.body.append(container);
|
|
817
|
-
return container;
|
|
912
|
+
function commitInput(element, text) {
|
|
913
|
+
typeInput(element, text);
|
|
914
|
+
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
818
915
|
}
|
|
819
916
|
/**
|
|
820
917
|
* Clears both browser storage surfaces.
|
|
@@ -834,6 +931,35 @@ function clearStorage() {
|
|
|
834
931
|
sessionStorage.clear();
|
|
835
932
|
}
|
|
836
933
|
/**
|
|
934
|
+
* Deletes one IndexedDB database and reports what the request actually did.
|
|
935
|
+
*
|
|
936
|
+
* @param name - The database name to delete.
|
|
937
|
+
* @returns A promise resolving after the deletion completes.
|
|
938
|
+
* @throws Thrown when the request errors, and when an open connection blocks it.
|
|
939
|
+
*
|
|
940
|
+
* @remarks
|
|
941
|
+
* Deleting a database that was never created succeeds, so this is safe to call from a teardown hook
|
|
942
|
+
* that runs whether or not the test reached the code that opens one.
|
|
943
|
+
*
|
|
944
|
+
* A block is a rejection rather than a wait. `blocked` fires when another connection is still open,
|
|
945
|
+
* and a suite that swallowed it would leave the next test reading the previous test's records
|
|
946
|
+
* through a database that reports itself deleted. The connection holding it open is the caller's to
|
|
947
|
+
* close, so the block is handed back rather than absorbed.
|
|
948
|
+
*
|
|
949
|
+
* @example
|
|
950
|
+
* ```ts
|
|
951
|
+
* afterEach(() => removeDatabase('ledger'))
|
|
952
|
+
* ```
|
|
953
|
+
*/
|
|
954
|
+
function removeDatabase(name) {
|
|
955
|
+
return new Promise((resolve, reject) => {
|
|
956
|
+
const request = globalThis.indexedDB.deleteDatabase(name);
|
|
957
|
+
request.addEventListener("success", () => resolve());
|
|
958
|
+
request.addEventListener("error", () => reject(/* @__PURE__ */ new Error(`IndexedDB database "${name}" could not be deleted`)));
|
|
959
|
+
request.addEventListener("blocked", () => reject(/* @__PURE__ */ new Error(`IndexedDB database "${name}" is blocked by an open connection`)));
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
837
963
|
* Parses one computed CSS color value into straight sRGB channels.
|
|
838
964
|
*
|
|
839
965
|
* @param value - A computed `rgb()`, `rgba()`, or `color(srgb …)` value.
|
|
@@ -877,6 +1003,80 @@ function parseColor(value) {
|
|
|
877
1003
|
]);
|
|
878
1004
|
}
|
|
879
1005
|
/**
|
|
1006
|
+
* Resolves any CSS color expression to straight sRGB channels, by asking the browser.
|
|
1007
|
+
*
|
|
1008
|
+
* @param value - Any value the `color` property accepts: a keyword, a hex triple, a `var()`
|
|
1009
|
+
* reference, a `color-mix()`, or an already-computed `rgb()`.
|
|
1010
|
+
* @returns The resolved color's channels, or `undefined` when the CSSOM refuses the value or the
|
|
1011
|
+
* computed result names no color {@link parseColor} speaks.
|
|
1012
|
+
*
|
|
1013
|
+
* @remarks
|
|
1014
|
+
* This is the live half of the pair {@link parseColor} opens. `parseColor` reads text and speaks
|
|
1015
|
+
* only the computed syntaxes a cascade hands back; this stages a probe element, hands it to the real
|
|
1016
|
+
* cascade, and reads back what the engine computed — which is the only way a keyword, a hex triple,
|
|
1017
|
+
* or a `var()` reference becomes channels at all. The read itself goes through `parseColor`, so both
|
|
1018
|
+
* halves agree on what a computed value means.
|
|
1019
|
+
*
|
|
1020
|
+
* The probe is mounted, because an unmounted element inherits nothing and a `var()` reference to a
|
|
1021
|
+
* token declared on `:root` would resolve to the initial value instead. It is removed in a `finally`,
|
|
1022
|
+
* so a value that throws on the way through leaves no node behind.
|
|
1023
|
+
*
|
|
1024
|
+
* Refusal is the CSSOM's: an expression it will not parse leaves the probe's inline `color` empty
|
|
1025
|
+
* and this returns `undefined`. A `var()` naming an undeclared custom property is not refused,
|
|
1026
|
+
* because the cascade accepts it and computes the inherited color, so a test that means to catch a
|
|
1027
|
+
* missing token asserts on {@link token} rather than on this.
|
|
1028
|
+
*
|
|
1029
|
+
* @example
|
|
1030
|
+
* ```ts
|
|
1031
|
+
* rgba('rebeccapurple') // [102, 51, 153, 1]
|
|
1032
|
+
* rgba('not-a-color') // undefined
|
|
1033
|
+
* ```
|
|
1034
|
+
*/
|
|
1035
|
+
function rgba(value) {
|
|
1036
|
+
const probe = mount(build("span"));
|
|
1037
|
+
try {
|
|
1038
|
+
probe.style.color = value;
|
|
1039
|
+
if (probe.style.color === "") return void 0;
|
|
1040
|
+
return parseColor(style(probe, "color"));
|
|
1041
|
+
} finally {
|
|
1042
|
+
probe.remove();
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Determines whether two colors render the same, within the rounding a browser does.
|
|
1047
|
+
*
|
|
1048
|
+
* @param first - A CSS color expression or an already-parsed color.
|
|
1049
|
+
* @param second - A CSS color expression or an already-parsed color.
|
|
1050
|
+
* @returns `true` when every channel and the alpha agree within the tolerance; `false` otherwise,
|
|
1051
|
+
* including when either side names no readable color.
|
|
1052
|
+
*
|
|
1053
|
+
* @remarks
|
|
1054
|
+
* Each string side is resolved through {@link rgba}, so a keyword, a token reference, and the
|
|
1055
|
+
* `rgb()` the engine computes for either of them compare equal without a test converting anything
|
|
1056
|
+
* first. A side that resolves to nothing makes the answer `false` rather than a throw, because this
|
|
1057
|
+
* is a predicate.
|
|
1058
|
+
*
|
|
1059
|
+
* The tolerance is half a channel step on the 0–255 scale, and the alpha is scaled onto that same
|
|
1060
|
+
* range before it is compared, so one number covers both. Half a step is what a composite of
|
|
1061
|
+
* translucent layers and a `color-mix()` round trip actually drift by; anything a reader could see
|
|
1062
|
+
* is further than that and reports unequal.
|
|
1063
|
+
*
|
|
1064
|
+
* @example
|
|
1065
|
+
* ```ts
|
|
1066
|
+
* colorEqual('rebeccapurple', 'rgb(102, 51, 153)') // true
|
|
1067
|
+
* colorEqual('red', [0, 0, 255, 1]) // false
|
|
1068
|
+
* ```
|
|
1069
|
+
*/
|
|
1070
|
+
function colorEqual(first, second) {
|
|
1071
|
+
const left = typeof first === "string" ? rgba(first) : first;
|
|
1072
|
+
const right = typeof second === "string" ? rgba(second) : second;
|
|
1073
|
+
if (left === void 0 || right === void 0) return false;
|
|
1074
|
+
const tolerance = .5;
|
|
1075
|
+
const [leftRed, leftGreen, leftBlue, leftAlpha] = left;
|
|
1076
|
+
const [rightRed, rightGreen, rightBlue, rightAlpha] = right;
|
|
1077
|
+
return Math.abs(leftRed - rightRed) <= tolerance && Math.abs(leftGreen - rightGreen) <= tolerance && Math.abs(leftBlue - rightBlue) <= tolerance && Math.abs(leftAlpha - rightAlpha) * 255 <= tolerance;
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
880
1080
|
* Composites one color over another.
|
|
881
1081
|
*
|
|
882
1082
|
* @param front - The color painted on top.
|
|
@@ -1102,12 +1302,24 @@ function readRing(control, worn) {
|
|
|
1102
1302
|
/**
|
|
1103
1303
|
* Collects every class token the stylesheets loaded into this document actually define.
|
|
1104
1304
|
*
|
|
1105
|
-
* @returns The set of class names reachable in the shipped cascade.
|
|
1305
|
+
* @returns The set of class names reachable in the shipped cascade, in {@link readRules} order.
|
|
1106
1306
|
*
|
|
1107
1307
|
* @remarks
|
|
1108
1308
|
* The set is what an authored-class conformance check measures against, so a class no loaded
|
|
1109
1309
|
* stylesheet defines — an invented utility, a misspelled framework name — is absent from it.
|
|
1110
1310
|
*
|
|
1311
|
+
* The tokens come from the {@link readRules} walk, which decides both the membership and the
|
|
1312
|
+
* insertion order this reader reports, and each answer is a deliberate difference from 0.0.8. A
|
|
1313
|
+
* class declared inside a grouping rule — a media query, a supports block, a layer, a nested style
|
|
1314
|
+
* rule — counts as defined, because a class the cascade defines under a condition is still one the
|
|
1315
|
+
* cascade defines; 0.0.8 read the top-level rules alone. Insertion order is breadth-first, so a
|
|
1316
|
+
* top-level class lands before a class declared inside an earlier grouping rule; 0.0.8 popped a
|
|
1317
|
+
* stack and inserted the deepest rule first. Iterate the set where the order is the subject, and
|
|
1318
|
+
* read `has` where membership is.
|
|
1319
|
+
*
|
|
1320
|
+
* `@keyframes` children are outside that walk, so an animation's own rules define no token here.
|
|
1321
|
+
* Reach the animation itself through {@link findKeyframes}.
|
|
1322
|
+
*
|
|
1111
1323
|
* @example
|
|
1112
1324
|
* ```ts
|
|
1113
1325
|
* readCascade().has('card')
|
|
@@ -1115,17 +1327,98 @@ function readRing(control, worn) {
|
|
|
1115
1327
|
*/
|
|
1116
1328
|
function readCascade() {
|
|
1117
1329
|
const known = /* @__PURE__ */ new Set();
|
|
1118
|
-
const
|
|
1119
|
-
for (const sheet of document.styleSheets) rules.push(...sheet.cssRules);
|
|
1120
|
-
while (rules.length > 0) {
|
|
1121
|
-
const rule = rules.pop();
|
|
1122
|
-
if (rule instanceof CSSGroupingRule) rules.push(...rule.cssRules);
|
|
1330
|
+
for (const rule of readRules()) {
|
|
1123
1331
|
if (!(rule instanceof CSSStyleRule)) continue;
|
|
1124
1332
|
for (const match of rule.selectorText.matchAll(/\.([a-zA-Z][\w-]*)/g)) known.add(String(match[1]));
|
|
1125
1333
|
}
|
|
1126
1334
|
return known;
|
|
1127
1335
|
}
|
|
1128
1336
|
/**
|
|
1337
|
+
* Collects every rule the stylesheets loaded into this document hold, nested grouping rules
|
|
1338
|
+
* included.
|
|
1339
|
+
*
|
|
1340
|
+
* @returns Every rule reachable in the shipped cascade: each sheet's own rules in sheet order, then
|
|
1341
|
+
* the rules nested inside them, level by level.
|
|
1342
|
+
*
|
|
1343
|
+
* @remarks
|
|
1344
|
+
* The walk is iterative and reads the list it is still appending to, which is what expands a media
|
|
1345
|
+
* query, a supports block, a layer, and a nested style rule without recursion. Expanding by level
|
|
1346
|
+
* rather than by depth is why a top-level rule is always met before a rule nested inside an earlier
|
|
1347
|
+
* one; {@link findRule} returns the first match in exactly this order.
|
|
1348
|
+
*
|
|
1349
|
+
* The descent reaches a `CSSGroupingRule` and nothing else, and a `@keyframes` rule is not one. The
|
|
1350
|
+
* `@keyframes` rule itself is collected wherever it sits, and the keyframe rules inside it are not;
|
|
1351
|
+
* {@link findKeyframes} is the door to those.
|
|
1352
|
+
*
|
|
1353
|
+
* A stylesheet the document cannot read — a cross-origin sheet with no CORS grant — throws from its
|
|
1354
|
+
* own `cssRules` getter, and that sheet is skipped rather than ending the walk. What a page loaded
|
|
1355
|
+
* from another origin declares is unreadable to every caller here, so the alternative is a helper
|
|
1356
|
+
* that works until a test page adds a font or an analytics stylesheet.
|
|
1357
|
+
*
|
|
1358
|
+
* @example
|
|
1359
|
+
* ```ts
|
|
1360
|
+
* readRules().filter((rule) => rule instanceof CSSKeyframesRule)
|
|
1361
|
+
* ```
|
|
1362
|
+
*/
|
|
1363
|
+
function readRules() {
|
|
1364
|
+
const rules = [];
|
|
1365
|
+
for (const sheet of document.styleSheets) try {
|
|
1366
|
+
rules.push(...sheet.cssRules);
|
|
1367
|
+
} catch {
|
|
1368
|
+
continue;
|
|
1369
|
+
}
|
|
1370
|
+
for (let index = 0; index < rules.length; index += 1) {
|
|
1371
|
+
const rule = rules[index];
|
|
1372
|
+
if (rule instanceof CSSGroupingRule) rules.push(...rule.cssRules);
|
|
1373
|
+
}
|
|
1374
|
+
return rules;
|
|
1375
|
+
}
|
|
1376
|
+
/**
|
|
1377
|
+
* Finds the first style rule in the cascade whose selector carries a fragment.
|
|
1378
|
+
*
|
|
1379
|
+
* @param selector - The selector fragment to look for, matched as a substring of the whole selector
|
|
1380
|
+
* text.
|
|
1381
|
+
* @returns The first matching rule in {@link readRules} order, or `undefined` when no rule carries
|
|
1382
|
+
* the fragment.
|
|
1383
|
+
*
|
|
1384
|
+
* @remarks
|
|
1385
|
+
* This proves a declaration exists in the cascade at all, which is a different question from what an
|
|
1386
|
+
* element resolves to: {@link style} reads the winner, and a rule this finds may be overridden by
|
|
1387
|
+
* another. Assert on this where the subject is the stylesheet, and on `style` where the subject is
|
|
1388
|
+
* the rendered result.
|
|
1389
|
+
*
|
|
1390
|
+
* The match is a substring, so `findRule('.card')` finds `.card`, `.card:hover`, and
|
|
1391
|
+
* `.panel > .card` alike. Pass more of the selector to narrow it.
|
|
1392
|
+
*
|
|
1393
|
+
* @example
|
|
1394
|
+
* ```ts
|
|
1395
|
+
* findRule('.card')?.style.getPropertyValue('padding')
|
|
1396
|
+
* ```
|
|
1397
|
+
*/
|
|
1398
|
+
function findRule(selector) {
|
|
1399
|
+
for (const rule of readRules()) if (rule instanceof CSSStyleRule && rule.selectorText.includes(selector)) return rule;
|
|
1400
|
+
}
|
|
1401
|
+
/**
|
|
1402
|
+
* Finds the animation the cascade declares under one name.
|
|
1403
|
+
*
|
|
1404
|
+
* @param name - The exact `@keyframes` name.
|
|
1405
|
+
* @returns The first matching rule in {@link readRules} order, or `undefined` when the cascade
|
|
1406
|
+
* declares no animation under that name.
|
|
1407
|
+
*
|
|
1408
|
+
* @remarks
|
|
1409
|
+
* The name is matched exactly, which is where this parts from {@link findRule}: a selector is
|
|
1410
|
+
* compound and a fragment of one is a useful question, and an animation name is one atom that either
|
|
1411
|
+
* is or is not the one an `animation` declaration references.
|
|
1412
|
+
*
|
|
1413
|
+
* @example
|
|
1414
|
+
* ```ts
|
|
1415
|
+
* findKeyframes('fade')?.cssRules.length
|
|
1416
|
+
* ```
|
|
1417
|
+
*/
|
|
1418
|
+
function findKeyframes(name) {
|
|
1419
|
+
for (const rule of readRules()) if (rule instanceof CSSKeyframesRule && rule.name === name) return rule;
|
|
1420
|
+
}
|
|
1421
|
+
/**
|
|
1129
1422
|
* Reads the normalized visible text of every element a selector matches, in document order.
|
|
1130
1423
|
*
|
|
1131
1424
|
* @param root - The subtree to search.
|
|
@@ -1185,8 +1478,13 @@ function extractOrphans(root, child, parent) {
|
|
|
1185
1478
|
* Reads one resolved CSS property from a real browser element.
|
|
1186
1479
|
*
|
|
1187
1480
|
* @param element - The element whose resolved style to inspect.
|
|
1188
|
-
* @param property - The CSS property name.
|
|
1189
|
-
* @returns The browser's resolved property value
|
|
1481
|
+
* @param property - The CSS property name, registered or custom.
|
|
1482
|
+
* @returns The browser's resolved property value, trimmed; an empty string when the element resolves
|
|
1483
|
+
* none.
|
|
1484
|
+
*
|
|
1485
|
+
* @remarks
|
|
1486
|
+
* The value is trimmed, so what comes back is the value and never the whitespace around it. Internal
|
|
1487
|
+
* whitespace is kept: `--shadow: 0 0 2px` reads back with its spaces.
|
|
1190
1488
|
*
|
|
1191
1489
|
* @example
|
|
1192
1490
|
* ```ts
|
|
@@ -1194,7 +1492,81 @@ function extractOrphans(root, child, parent) {
|
|
|
1194
1492
|
* ```
|
|
1195
1493
|
*/
|
|
1196
1494
|
function style(element, property) {
|
|
1197
|
-
return getComputedStyle(element).getPropertyValue(property);
|
|
1495
|
+
return getComputedStyle(element).getPropertyValue(property).trim();
|
|
1496
|
+
}
|
|
1497
|
+
/**
|
|
1498
|
+
* Reads one custom property from an element's resolved style.
|
|
1499
|
+
*
|
|
1500
|
+
* @param element - The element whose resolved style to inspect.
|
|
1501
|
+
* @param name - The custom property name, with or without its leading dashes.
|
|
1502
|
+
* @returns The resolved value, trimmed; an empty string when the element inherits no such property.
|
|
1503
|
+
*
|
|
1504
|
+
* @remarks
|
|
1505
|
+
* The dashes are optional because a token is spoken about both ways — `--surface` in a stylesheet
|
|
1506
|
+
* and `surface` in prose — and a reader that accepted only one spelling would turn that into a silent
|
|
1507
|
+
* empty string. An absent token reads as `''`, which is what the CSSOM returns and is
|
|
1508
|
+
* indistinguishable from a token declared empty; assert on the value you expect rather than on
|
|
1509
|
+
* presence.
|
|
1510
|
+
*
|
|
1511
|
+
* Resolution is inheritance, so a token declared on `:root` reads from any mounted descendant and
|
|
1512
|
+
* from an unmounted element reads as `''`. Use {@link rootToken} where the declaration is the
|
|
1513
|
+
* document's.
|
|
1514
|
+
*
|
|
1515
|
+
* @example
|
|
1516
|
+
* ```ts
|
|
1517
|
+
* token(panel, 'surface') // '#ffffff'
|
|
1518
|
+
* token(panel, '--surface') // '#ffffff'
|
|
1519
|
+
* ```
|
|
1520
|
+
*/
|
|
1521
|
+
function token(element, name) {
|
|
1522
|
+
return style(element, name.startsWith("--") ? name : `--${name}`);
|
|
1523
|
+
}
|
|
1524
|
+
/**
|
|
1525
|
+
* Reads one custom property from the document element.
|
|
1526
|
+
*
|
|
1527
|
+
* @param name - The custom property name, with or without its leading dashes.
|
|
1528
|
+
* @returns The resolved value, trimmed; an empty string when the document declares no such property.
|
|
1529
|
+
*
|
|
1530
|
+
* @remarks
|
|
1531
|
+
* This is {@link token} against `document.documentElement`, which is where a theme declares its
|
|
1532
|
+
* tokens and where a `[data-theme]` switch retunes them. It exists as its own name because that
|
|
1533
|
+
* element is the one a token question is nearly always about, and naming it at every call site
|
|
1534
|
+
* buries the question.
|
|
1535
|
+
*
|
|
1536
|
+
* @example
|
|
1537
|
+
* ```ts
|
|
1538
|
+
* rootToken('surface')
|
|
1539
|
+
* ```
|
|
1540
|
+
*/
|
|
1541
|
+
function rootToken(name) {
|
|
1542
|
+
return token(document.documentElement, name);
|
|
1543
|
+
}
|
|
1544
|
+
/**
|
|
1545
|
+
* Reads one resolved CSS length as a number of pixels.
|
|
1546
|
+
*
|
|
1547
|
+
* @param element - The element whose resolved style to inspect.
|
|
1548
|
+
* @param property - The CSS property name, registered or custom.
|
|
1549
|
+
* @returns The leading numeric part of the resolved value, and `0` when it carries none.
|
|
1550
|
+
*
|
|
1551
|
+
* @remarks
|
|
1552
|
+
* A resolved length is text with a unit — `'12px'` — so this reads the number in front of the unit
|
|
1553
|
+
* and discards the rest. The unit is not checked: the resolved value of a length is in pixels in
|
|
1554
|
+
* every case a browser hands back, and a property that resolves to something else is the caller's
|
|
1555
|
+
* mistake rather than this reader's.
|
|
1556
|
+
*
|
|
1557
|
+
* An unparsable value reads as `0` rather than as absence, because every caller of this is measuring
|
|
1558
|
+
* and `'auto'`, `'none'`, and `''` each contribute no pixels to what a reader sees. Where the
|
|
1559
|
+
* distinction matters, read the text with {@link style} instead.
|
|
1560
|
+
*
|
|
1561
|
+
* @example
|
|
1562
|
+
* ```ts
|
|
1563
|
+
* pixels(button, 'padding-left') // 12
|
|
1564
|
+
* pixels(button, 'width') // 0 when the width resolves to `auto`
|
|
1565
|
+
* ```
|
|
1566
|
+
*/
|
|
1567
|
+
function pixels(element, property) {
|
|
1568
|
+
const measured = Number.parseFloat(style(element, property));
|
|
1569
|
+
return Number.isFinite(measured) ? measured : 0;
|
|
1198
1570
|
}
|
|
1199
1571
|
/**
|
|
1200
1572
|
* Sets the tester's viewport and renders the runner's pane at the size that viewport claims.
|
|
@@ -1355,6 +1727,72 @@ function expandCaptures(states, variants) {
|
|
|
1355
1727
|
//#endregion
|
|
1356
1728
|
//#region src/browser/factories.ts
|
|
1357
1729
|
/**
|
|
1730
|
+
* Creates one real pointer event, ready to dispatch.
|
|
1731
|
+
*
|
|
1732
|
+
* @param name - The event type, such as `pointerdown`.
|
|
1733
|
+
* @param options - Any `PointerEventInit` member, each one overriding the default beneath it.
|
|
1734
|
+
* @returns A real `PointerEvent` of that type.
|
|
1735
|
+
*
|
|
1736
|
+
* @remarks
|
|
1737
|
+
* The defaults are what a browser's own pointer event carries and a hand-built one does not:
|
|
1738
|
+
* `bubbles` and `cancelable` are set, so a delegated listener hears it and a handler can prevent it,
|
|
1739
|
+
* and `pointerId`, `pointerType`, and `isPrimary` describe a single primary mouse, so a component
|
|
1740
|
+
* that branches on the pointer kind takes the branch a mouse takes. Override any of them by naming
|
|
1741
|
+
* it; a touch is `{ pointerType: 'touch' }` and nothing else has to be restated.
|
|
1742
|
+
*
|
|
1743
|
+
* The event is real rather than a shaped object, so `instanceof PointerEvent` holds and the
|
|
1744
|
+
* coordinate and modifier members a handler reads are the ones the platform defines.
|
|
1745
|
+
*
|
|
1746
|
+
* @example
|
|
1747
|
+
* ```ts
|
|
1748
|
+
* element.dispatchEvent(createPointerEvent('pointerdown', { clientX: 10, clientY: 20 }))
|
|
1749
|
+
* ```
|
|
1750
|
+
*/
|
|
1751
|
+
function createPointerEvent(name, options) {
|
|
1752
|
+
return new PointerEvent(name, {
|
|
1753
|
+
bubbles: true,
|
|
1754
|
+
cancelable: true,
|
|
1755
|
+
pointerId: 1,
|
|
1756
|
+
pointerType: "mouse",
|
|
1757
|
+
isPrimary: true,
|
|
1758
|
+
...options
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
/**
|
|
1762
|
+
* Creates one real drag event carrying a live data transfer, ready to dispatch.
|
|
1763
|
+
*
|
|
1764
|
+
* @param name - The event type, such as `dragstart`.
|
|
1765
|
+
* @param options - Any `DragEventInit` member, each one overriding the default beneath it.
|
|
1766
|
+
* @returns A real `DragEvent` of that type.
|
|
1767
|
+
*
|
|
1768
|
+
* @remarks
|
|
1769
|
+
* A drag event with no `dataTransfer` is the shape that makes a drop handler fail in a test and work
|
|
1770
|
+
* in a browser, so one is allocated. Pass your own to seed it: a `dataTransfer` given in `options`
|
|
1771
|
+
* replaces the allocated one, which is how a drop is driven with the payload the drag was supposed
|
|
1772
|
+
* to carry.
|
|
1773
|
+
*
|
|
1774
|
+
* The platform declares the `dataTransfer` member on the constructed event as nullable, so calling
|
|
1775
|
+
* code still narrows it even though this always supplies one.
|
|
1776
|
+
*
|
|
1777
|
+
* `bubbles` and `cancelable` are set, because a drop handler that never prevents the default event
|
|
1778
|
+
* is a drop the browser handles itself.
|
|
1779
|
+
*
|
|
1780
|
+
* @example
|
|
1781
|
+
* ```ts
|
|
1782
|
+
* const started = createDragEvent('dragstart')
|
|
1783
|
+
* started.dataTransfer?.setData('text/plain', 'row-3')
|
|
1784
|
+
* element.dispatchEvent(started)
|
|
1785
|
+
* ```
|
|
1786
|
+
*/
|
|
1787
|
+
function createDragEvent(name, options) {
|
|
1788
|
+
return new DragEvent(name, {
|
|
1789
|
+
bubbles: true,
|
|
1790
|
+
cancelable: true,
|
|
1791
|
+
dataTransfer: new DataTransfer(),
|
|
1792
|
+
...options
|
|
1793
|
+
});
|
|
1794
|
+
}
|
|
1795
|
+
/**
|
|
1358
1796
|
* Creates the capture portfolio one run places its screenshots through.
|
|
1359
1797
|
*
|
|
1360
1798
|
* @param options - The state registry, the variant matrix, the variant this run renders, the
|
|
@@ -1419,6 +1857,36 @@ function createPortfolio(options) {
|
|
|
1419
1857
|
};
|
|
1420
1858
|
}
|
|
1421
1859
|
/**
|
|
1860
|
+
* Creates one console channel that records every call it receives and hands that call on unchanged.
|
|
1861
|
+
*
|
|
1862
|
+
* @param name - The channel's name, which prefixes each line it records.
|
|
1863
|
+
* @param output - The list each call is recorded into, appended to in place.
|
|
1864
|
+
* @param forward - The channel every call is passed on to after it is recorded.
|
|
1865
|
+
* @returns A channel carrying the console's own call signature.
|
|
1866
|
+
*
|
|
1867
|
+
* @remarks
|
|
1868
|
+
* One call becomes one line. Every argument of that call is put through `String` and joined with a
|
|
1869
|
+
* space, so a call carrying several values reads as the one line the page printed rather than as
|
|
1870
|
+
* several entries.
|
|
1871
|
+
*
|
|
1872
|
+
* Nothing is swallowed. The record happens first and `forward` receives the arguments it would have
|
|
1873
|
+
* received, so a page recorded through this prints exactly what it printed without it. The list
|
|
1874
|
+
* belongs to the caller, so a channel writes into whatever it was handed and holds no state of its
|
|
1875
|
+
* own. {@link createJournal} builds one channel per console method over one list.
|
|
1876
|
+
*
|
|
1877
|
+
* @example
|
|
1878
|
+
* ```ts
|
|
1879
|
+
* const output: string[] = []
|
|
1880
|
+
* console.log = createChannel('log', output, console.log)
|
|
1881
|
+
* ```
|
|
1882
|
+
*/
|
|
1883
|
+
function createChannel(name, output, forward) {
|
|
1884
|
+
return (...data) => {
|
|
1885
|
+
output.push(`${name}: ${data.map((value) => String(value)).join(" ")}`);
|
|
1886
|
+
forward(...data);
|
|
1887
|
+
};
|
|
1888
|
+
}
|
|
1889
|
+
/**
|
|
1422
1890
|
* Creates the journal one scenario records its steps and the page's own output into.
|
|
1423
1891
|
*
|
|
1424
1892
|
* @returns A journal that records nothing until it is started.
|
|
@@ -1472,10 +1940,7 @@ function createJournal() {
|
|
|
1472
1940
|
"info",
|
|
1473
1941
|
"log",
|
|
1474
1942
|
"warn"
|
|
1475
|
-
]) console[channel] = (
|
|
1476
|
-
output.push(`${channel}: ${data.map((value) => String(value)).join(" ")}`);
|
|
1477
|
-
forwarded[channel](...data);
|
|
1478
|
-
};
|
|
1943
|
+
]) console[channel] = createChannel(channel, output, forwarded[channel]);
|
|
1479
1944
|
const dropped = new AbortController();
|
|
1480
1945
|
listeners = dropped;
|
|
1481
1946
|
window.addEventListener("error", (event) => {
|
|
@@ -1503,6 +1968,6 @@ function createJournal() {
|
|
|
1503
1968
|
};
|
|
1504
1969
|
}
|
|
1505
1970
|
//#endregion
|
|
1506
|
-
export { ACCESSIBLE_ROLES, CANVAS_COLOR, CAPTURE_PANE, CONTENT_ROLES, FIELD_ROLES, FOCUSABLE_SELECTOR, HEADER_ROLES, IMPLICIT_ROLES, blendColor, captureFrame, clearStorage, clickAccessible, clickAccessibleWithin, clickDisclosure, contrast, createJournal, createPortfolio, describeFocus, describeTree, expandCaptures, extractOrphans, fillAccessible, isOutsideViewport, isReachable, isRendered, measureContrast, measureLuminance, parseColor, pressKeys, readBackdrop, readCascade, readFocus, readLayers, readName, readPage, readPerception, readRing, readRole, readRows, readStates, readText, readValue, releasePane, render, resolveAccessible, resolveRendered, stagePane, style, traverseAccessible, typeAccessible, waitForFrame };
|
|
1971
|
+
export { ACCESSIBLE_ROLES, CANVAS_COLOR, CAPTURE_PANE, CONTENT_ROLES, FIELD_ROLES, FOCUSABLE_SELECTOR, HEADER_ROLES, IMPLICIT_ROLES, blendColor, build, captureFrame, clearStorage, clickAccessible, clickAccessibleWithin, clickDisclosure, colorEqual, commitInput, contrast, createChannel, createDragEvent, createJournal, createPointerEvent, createPortfolio, describeFocus, describeTree, expandCaptures, extractOrphans, fillAccessible, findKeyframes, findRule, isOutsideViewport, isReachable, isRendered, measureContrast, measureLuminance, mount, parseColor, pixels, pressKeys, readBackdrop, readCascade, readFocus, readLayers, readName, readPage, readPerception, readRing, readRole, readRows, readRules, readStates, readText, readValue, releasePane, removeDatabase, render, resolveAccessible, resolveRendered, rgba, rootToken, stagePane, style, token, traverseAccessible, typeAccessible, typeInput, waitForFrame };
|
|
1507
1972
|
|
|
1508
1973
|
//# sourceMappingURL=index.js.map
|