@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.
@@ -799,22 +799,119 @@ function waitForFrame() {
799
799
  return new Promise((resolve) => requestAnimationFrame(() => resolve()));
800
800
  }
801
801
  /**
802
- * Renders trusted fixture markup into a container attached to the document.
802
+ * Builds one unmounted element of a known tag, wearing the classes, text, and attributes asked for.
803
803
  *
804
- * @param markup - The fixture markup to render.
805
- * @returns The attached container.
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
- * const container = render('<button type="button">Save</button>')
810
- * container.remove()
909
+ * commitInput(requireValue(container.querySelector('input')), 'Ada')
811
910
  * ```
812
911
  */
813
- function render(markup) {
814
- const container = document.createElement("div");
815
- container.innerHTML = markup;
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 rules = [];
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] = (...data) => {
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