@schukai/monster 4.152.0 → 4.153.1

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.
Files changed (52) hide show
  1. package/README.md +24 -1
  2. package/package.json +1 -1
  3. package/source/components/content/viewer/message.mjs +1 -0
  4. package/source/components/form/select.mjs +2 -0
  5. package/source/components/form/util/fetch.mjs +6 -7
  6. package/source/components/state/log.mjs +7 -0
  7. package/source/components/state/thread.mjs +11 -0
  8. package/source/components/time/month-calendar.mjs +80 -30
  9. package/source/data/buildmap.mjs +3 -2
  10. package/source/data/buildtree.mjs +0 -8
  11. package/source/data/pathfinder.mjs +48 -19
  12. package/source/data/transformer.mjs +2 -2
  13. package/source/dom/customcontrol.mjs +35 -9
  14. package/source/dom/customelement.mjs +21 -2
  15. package/source/dom/resource.mjs +19 -5
  16. package/source/dom/sanitize-html.mjs +34 -29
  17. package/source/dom/slotted.mjs +46 -31
  18. package/source/dom/template.mjs +4 -1
  19. package/source/dom/updater.mjs +51 -15
  20. package/source/types/node.mjs +1 -3
  21. package/source/types/proxyobserver.mjs +32 -10
  22. package/test/cases/components/datatable/drag-scroll.mjs +28 -1
  23. package/test/cases/components/form/button-bar.mjs +9 -6
  24. package/test/cases/components/form/confirm-button.mjs +4 -4
  25. package/test/cases/components/form/control-bar.mjs +73 -72
  26. package/test/cases/components/form/input-group.mjs +3 -1
  27. package/test/cases/components/form/password.mjs +2 -2
  28. package/test/cases/components/form/popper-button.mjs +2 -2
  29. package/test/cases/components/form/select-sort-callback.mjs +57 -0
  30. package/test/cases/components/form/select.mjs +2 -2
  31. package/test/cases/components/form/sheet.mjs +13 -1
  32. package/test/cases/components/layout/popper.mjs +3 -1
  33. package/test/cases/components/layout/tabs.mjs +4 -1
  34. package/test/cases/components/navigation/site-navigation.mjs +12 -26
  35. package/test/cases/components/navigation/table-of-content.mjs +3 -0
  36. package/test/cases/components/state/ticker-lifecycle.mjs +47 -0
  37. package/test/cases/components/time/month-calendar.mjs +100 -0
  38. package/test/cases/data/buildmap.mjs +14 -0
  39. package/test/cases/data/buildtree.mjs +49 -0
  40. package/test/cases/data/pathfinder.mjs +34 -0
  41. package/test/cases/data/transformer.mjs +33 -0
  42. package/test/cases/dom/customcontrol.mjs +87 -6
  43. package/test/cases/dom/customelement.mjs +54 -0
  44. package/test/cases/dom/ready.mjs +3 -3
  45. package/test/cases/dom/resource-availability.mjs +46 -0
  46. package/test/cases/dom/resourcemanager.mjs +12 -6
  47. package/test/cases/dom/sanitize-html.mjs +71 -0
  48. package/test/cases/dom/slotted-nodes.mjs +48 -1
  49. package/test/cases/dom/template.mjs +11 -0
  50. package/test/cases/dom/updater.mjs +119 -1
  51. package/test/cases/types/proxyobserver.mjs +75 -0
  52. package/test/cases/util/processing.mjs +3 -3
@@ -16,39 +16,44 @@ export function sanitizeHtml(htmlString, options = {}) {
16
16
  );
17
17
 
18
18
  const blockedTags = options.blockedTags || [];
19
-
20
- blockedTags.forEach((tag) => {
21
- doc.querySelectorAll(tag).forEach((el) => el.remove());
22
- });
23
-
24
- // remove dangerous attributes
25
- const dangerousAttr = /^on/i;
26
- const urlAttrs = ["xlink:href", "action", "formaction"];
27
-
28
- doc.querySelectorAll("*").forEach((el) => {
29
- // remove dangerous attributes
30
- [...el.attributes].forEach((attr) => {
31
- const name = attr.name;
32
- const value = attr.value;
33
-
34
- // remove attributes that start with "on" (event handlers)
35
- if (dangerousAttr.test(name)) {
36
- el.removeAttribute(name);
19
+ const animationTags = new Set([
20
+ "animate",
21
+ "set",
22
+ "animatemotion",
23
+ "animatetransform",
24
+ "animatecolor",
25
+ "discard",
26
+ ]);
27
+ const urlAttrs = new Set(["href", "xlink:href", "action", "formaction"]);
28
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: URL schemes must be checked after removing control characters.
29
+ const urlControlCharacters = /[\u0000-\u0020\u007f-\u009f]/g;
30
+
31
+ function sanitizeTree(root) {
32
+ for (const tag of blockedTags) {
33
+ for (const el of root.querySelectorAll(tag)) el.remove();
34
+ }
35
+ for (const el of root.querySelectorAll("*")) {
36
+ // SVG animations can change a safe URL after the attribute check.
37
+ if (animationTags.has(el.localName.toLowerCase())) {
38
+ el.remove();
39
+ continue;
37
40
  }
38
-
39
- // remove URL attributes that start with javascript:, data:, or vbscript:
40
- if (urlAttrs.includes(name)) {
41
- const val = value.trim().toLowerCase();
42
- if (
43
- val.startsWith("javascript:") ||
44
- val.startsWith("data:") ||
45
- val.startsWith("vbscript:")
46
- ) {
41
+ for (const { name, value } of [...el.attributes]) {
42
+ if (/^on/i.test(name)) {
47
43
  el.removeAttribute(name);
44
+ } else if (urlAttrs.has(name.toLowerCase())) {
45
+ const normalized = value.replace(urlControlCharacters, "").toLowerCase();
46
+ if (/^(javascript|data|vbscript):/.test(normalized)) {
47
+ el.removeAttribute(name);
48
+ }
48
49
  }
49
50
  }
50
- });
51
- });
51
+ if (el.localName === "template" && el.content) {
52
+ sanitizeTree(el.content);
53
+ }
54
+ }
55
+ }
56
+ sanitizeTree(doc);
52
57
 
53
58
  return doc.body.innerHTML;
54
59
  }
@@ -27,37 +27,33 @@ export { getSlottedElements, getSlottedNodes };
27
27
  * @since 3.33.0
28
28
  * @throws {Error} query must be a string
29
29
  */
30
- function getSlottedNodes(query, name) {
30
+ function getSlottedNodes(query, name, managedShadowRoot) {
31
31
  const result = new Set();
32
+ const shadowRoot = getShadowRoot(this, managedShadowRoot);
32
33
 
33
- if (!this.shadowRoot) {
34
+ if (!(shadowRoot instanceof ShadowRoot)) {
34
35
  return result;
35
36
  }
36
37
 
37
- let selector = "slot";
38
- if (name !== undefined) {
39
- if (name === null) {
40
- selector += ":not([name])";
41
- } else {
42
- selector += `[name=${validateString(name)}]`;
43
- }
44
- }
45
-
46
- const slots = this.shadowRoot.querySelectorAll(selector);
38
+ const slots = getSlotsByName(shadowRoot, name);
47
39
 
48
- for (const [, slot] of Object.entries(slots)) {
40
+ for (const slot of slots) {
49
41
  slot.assignedNodes().forEach(function (node) {
50
42
  if (node === null || node === undefined) {
51
43
  return;
52
44
  }
53
45
 
54
46
  if (isString(query)) {
55
- node.querySelectorAll(query).forEach(function (n) {
56
- result.add(n);
57
- });
58
-
59
- if (node.matches(query)) {
47
+ if (query.length === 0) {
60
48
  result.add(node);
49
+ } else if (node instanceof Element) {
50
+ node.querySelectorAll(query).forEach(function (n) {
51
+ result.add(n);
52
+ });
53
+
54
+ if (node.matches(query)) {
55
+ result.add(node);
56
+ }
61
57
  }
62
58
  } else if (query !== undefined) {
63
59
  throw new Error("query must be a string");
@@ -80,25 +76,17 @@ function getSlottedNodes(query, name) {
80
76
  * @since 1.23.0
81
77
  * @throws {Error} query must be a string
82
78
  */
83
- function getSlottedElements(query, name) {
79
+ function getSlottedElements(query, name, managedShadowRoot) {
84
80
  const result = new Set();
81
+ const shadowRoot = getShadowRoot(this, managedShadowRoot);
85
82
 
86
- if (!(this.shadowRoot instanceof ShadowRoot)) {
83
+ if (!(shadowRoot instanceof ShadowRoot)) {
87
84
  return result;
88
85
  }
89
86
 
90
- let selector = "slot";
91
- if (name !== undefined) {
92
- if (name === null) {
93
- selector += ":not([name])";
94
- } else {
95
- selector += `[name=${validateString(name)}]`;
96
- }
97
- }
98
-
99
- const slots = this.shadowRoot.querySelectorAll(selector);
87
+ const slots = getSlotsByName(shadowRoot, name);
100
88
 
101
- for (const [, slot] of Object.entries(slots)) {
89
+ for (const slot of slots) {
102
90
  slot.assignedElements().forEach(function (node) {
103
91
  if (
104
92
  !(node instanceof HTMLElement) &&
@@ -129,3 +117,30 @@ function getSlottedElements(query, name) {
129
117
 
130
118
  return result;
131
119
  }
120
+
121
+ /**
122
+ * @param {*} host
123
+ * @param {*} managedShadowRoot
124
+ * @return {ShadowRoot|null}
125
+ * @private
126
+ */
127
+ function getShadowRoot(host, managedShadowRoot) {
128
+ if (managedShadowRoot instanceof ShadowRoot) return managedShadowRoot;
129
+ return host?.shadowRoot instanceof ShadowRoot ? host.shadowRoot : null;
130
+ }
131
+
132
+ /**
133
+ * Filter slots by their DOM name instead of interpolating an unsafe selector.
134
+ * @param {ShadowRoot} shadowRoot
135
+ * @param {string|undefined|null} name
136
+ * @return {HTMLSlotElement[]}
137
+ * @private
138
+ */
139
+ function getSlotsByName(shadowRoot, name) {
140
+ if (name !== undefined && name !== null) validateString(name);
141
+ return Array.from(shadowRoot.querySelectorAll("slot")).filter((slot) => {
142
+ if (name === undefined) return true;
143
+ if (name === null) return slot.name === "";
144
+ return slot.name === name;
145
+ });
146
+ }
@@ -156,7 +156,10 @@ export function findDocumentTemplate(id, currentNode) {
156
156
  )
157
157
  ) {
158
158
  if (currentNode instanceof Node) {
159
- if (currentNode.hasAttribute(ATTRIBUTE_TEMPLATE_PREFIX)) {
159
+ if (
160
+ currentNode instanceof Element &&
161
+ currentNode.hasAttribute(ATTRIBUTE_TEMPLATE_PREFIX)
162
+ ) {
160
163
  prefixID = currentNode.getAttribute(ATTRIBUTE_TEMPLATE_PREFIX);
161
164
  }
162
165
 
@@ -803,17 +803,34 @@ function parseIntArray(val) {
803
803
  * @private
804
804
  */
805
805
  function retrieveFromBindings() {
806
- if (this[internalSymbol].element.matches(`[${ATTRIBUTE_UPDATER_BIND}]`)) {
806
+ if (
807
+ this[internalSymbol].element.matches(`[${ATTRIBUTE_UPDATER_BIND}]`) &&
808
+ !isUncheckedRadio(this[internalSymbol].element)
809
+ ) {
807
810
  retrieveAndSetValue.call(this, this[internalSymbol].element);
808
811
  }
809
812
 
810
813
  for (const [, element] of this[internalSymbol].element
811
814
  .querySelectorAll(`[${ATTRIBUTE_UPDATER_BIND}]`)
812
815
  .entries()) {
816
+ if (isUncheckedRadio(element)) continue;
813
817
  retrieveAndSetValue.call(this, element);
814
818
  }
815
819
  }
816
820
 
821
+ /**
822
+ * @param {HTMLElement} element
823
+ * @return {boolean}
824
+ * @private
825
+ */
826
+ function isUncheckedRadio(element) {
827
+ return (
828
+ element instanceof HTMLInputElement &&
829
+ element.type === "radio" &&
830
+ element.checked === false
831
+ );
832
+ }
833
+
817
834
  /**
818
835
  * @private
819
836
  * @license AGPLv3
@@ -1201,7 +1218,10 @@ function getSlots(container, cache) {
1201
1218
  return cache.get(container);
1202
1219
  }
1203
1220
 
1204
- const slots = container.querySelectorAll("slot");
1221
+ const slots = Array.from(container.querySelectorAll("slot"));
1222
+ if (container.localName === "slot") {
1223
+ slots.unshift(container);
1224
+ }
1205
1225
  cache.set(container, slots);
1206
1226
  return slots;
1207
1227
  }
@@ -1565,6 +1585,9 @@ function replaceKeyedPatchedChildren(
1565
1585
  const nextNode = preparePatchedNode(reused, renderedValue);
1566
1586
  nextNode[patchNodeKeySymbol] = normalizedKey;
1567
1587
  if (nextNode !== reused && reused.parentNode === element) {
1588
+ if (reused instanceof HTMLElement) {
1589
+ teardownManagedSubtree(reused);
1590
+ }
1568
1591
  element.replaceChild(nextNode, reused);
1569
1592
  }
1570
1593
  nextNodes.push(nextNode);
@@ -1591,15 +1614,11 @@ function replaceKeyedPatchedChildren(
1591
1614
  }
1592
1615
  }
1593
1616
 
1594
- for (const node of nextNodes) {
1595
- if (node.parentNode !== element) {
1596
- element.appendChild(node);
1597
- continue;
1598
- }
1599
-
1600
- if (element.lastChild !== node) {
1601
- element.appendChild(node);
1602
- }
1617
+ for (let index = 0; index < nextNodes.length; index++) {
1618
+ const node = nextNodes[index];
1619
+ const current = element.childNodes[index] ?? null;
1620
+ if (current === node) continue;
1621
+ element.insertBefore(node, current);
1603
1622
  }
1604
1623
  }
1605
1624
 
@@ -1851,8 +1870,7 @@ function runUpdateAttributes(
1851
1870
  if (
1852
1871
  !element.hasAttribute(ATTRIBUTE_UPDATER_SELECT_THIS) &&
1853
1872
  !attributes.includes(`path:${current}`) &&
1854
- !attributes.startsWith("static:") &&
1855
- !attributes.startsWith("i18n:")
1873
+ !hasUnconditionalBinding(attributes)
1856
1874
  ) {
1857
1875
  continue;
1858
1876
  }
@@ -1933,8 +1951,7 @@ function runUpdateProperties(
1933
1951
  if (
1934
1952
  !element.hasAttribute(ATTRIBUTE_UPDATER_SELECT_THIS) &&
1935
1953
  !properties.includes(`path:${current}`) &&
1936
- !properties.startsWith("static:") &&
1937
- !properties.startsWith("i18n:")
1954
+ !hasUnconditionalBinding(properties)
1938
1955
  ) {
1939
1956
  continue;
1940
1957
  }
@@ -1981,6 +1998,9 @@ function runUpdateProperties(
1981
1998
 
1982
1999
  function handleInputControlAttributeUpdate(element, name, value) {
1983
2000
  if (element instanceof HTMLSelectElement) {
2001
+ if (name !== "value") {
2002
+ return;
2003
+ }
1984
2004
  switch (element.type) {
1985
2005
  case "select-multiple":
1986
2006
  const selectedValues = isArray(value)
@@ -2054,6 +2074,22 @@ function handleInputControlAttributeUpdate(element, name, value) {
2054
2074
  }
2055
2075
  }
2056
2076
 
2077
+ /**
2078
+ * Check whether an attribute/property definition contains a command that is
2079
+ * independent from the changed subject path.
2080
+ * @param {string} definitions
2081
+ * @return {boolean}
2082
+ * @private
2083
+ */
2084
+ function hasUnconditionalBinding(definitions) {
2085
+ return definitions.split(",").some((definition) => {
2086
+ const separator = definition.indexOf(" ");
2087
+ if (separator < 0) return false;
2088
+ const command = trimSpaces(definition.substring(separator + 1));
2089
+ return command.startsWith("static:") || command.startsWith("i18n:");
2090
+ });
2091
+ }
2092
+
2057
2093
  /**
2058
2094
  * @private
2059
2095
  * @param {HTMLElement|*} element
@@ -199,8 +199,6 @@ class Node extends Base {
199
199
  * @return {setChildLevelAndParent}
200
200
  */
201
201
  function setChildLevelAndParent(node, operand, visitedNodes) {
202
- const self = this;
203
-
204
202
  if (visitedNodes.has(node)) {
205
203
  throw new Error(
206
204
  "the node has already been visited and cannot be traversed again",
@@ -217,7 +215,7 @@ function setChildLevelAndParent(node, operand, visitedNodes) {
217
215
  child[treeStructureSymbol].parent = node;
218
216
  child[treeStructureSymbol].level =
219
217
  node[treeStructureSymbol].level + operand;
220
- setChildLevelAndParent.call(self, child, operand, visitedNodes);
218
+ setChildLevelAndParent.call(node, child, operand, visitedNodes);
221
219
  });
222
220
  return this;
223
221
  }
@@ -156,6 +156,15 @@ function getHandler() {
156
156
  }
157
157
 
158
158
  const value = Reflect.get(target, key, receiver);
159
+ const descriptor = Reflect.getOwnPropertyDescriptor(target, key);
160
+ if (
161
+ descriptor &&
162
+ descriptor.configurable === false &&
163
+ "value" in descriptor &&
164
+ descriptor.writable === false
165
+ ) {
166
+ return value;
167
+ }
159
168
 
160
169
  if (typeof key === "symbol") {
161
170
  return value;
@@ -233,16 +242,12 @@ function getHandler() {
233
242
 
234
243
  // https://262.ecma-international.org/9.0/#sec-proxy-object-internal-methods-and-internal-slots-delete-p
235
244
  deleteProperty: function (target, key) {
236
- if (key in target) {
237
- delete target[key];
238
-
239
- if (typeof key !== "symbol") {
240
- proxy.observers.notify(proxy);
241
- }
242
-
243
- return true;
245
+ const hadOwnProperty = Object.hasOwn(target, key);
246
+ const result = Reflect.deleteProperty(target, key);
247
+ if (result && hadOwnProperty && typeof key !== "symbol") {
248
+ proxy.observers.notify(proxy);
244
249
  }
245
- return false;
250
+ return result;
246
251
  },
247
252
 
248
253
  // https://262.ecma-international.org/9.0/#sec-proxy-object-internal-methods-and-internal-slots-defineownproperty-p-desc
@@ -277,5 +282,22 @@ function getHandler() {
277
282
  * @private
278
283
  */
279
284
  function isNativeObjectWithInternalSlots(value) {
280
- return value instanceof Date || value instanceof Map || value instanceof Set;
285
+ if (
286
+ value instanceof Date ||
287
+ value instanceof RegExp ||
288
+ value instanceof Map ||
289
+ value instanceof Set ||
290
+ value instanceof WeakMap ||
291
+ value instanceof WeakSet ||
292
+ value instanceof Promise ||
293
+ value instanceof ArrayBuffer ||
294
+ ArrayBuffer.isView(value)
295
+ ) {
296
+ return true;
297
+ }
298
+
299
+ return (
300
+ typeof SharedArrayBuffer === "function" &&
301
+ value instanceof SharedArrayBuffer
302
+ );
281
303
  }
@@ -6,6 +6,21 @@ import { ResizeObserverMock } from "../../../util/resize-observer.mjs";
6
6
  const expect = chai.expect;
7
7
  chai.use(chaiDom);
8
8
 
9
+ // Supply geometry before the first layout can populate the measurement cache.
10
+ function installColumnBarGeometry(parentWidth) {
11
+ const original = HTMLElement.prototype.getBoundingClientRect;
12
+ HTMLElement.prototype.getBoundingClientRect = function () {
13
+ if (this.localName === "monster-column-bar") return {width:0};
14
+ if (this.getRootNode().host?.localName === "monster-column-bar") {
15
+ const role = this.getAttribute("data-monster-role");
16
+ if (role === "control") return {width:parentWidth()};
17
+ if (role === "settings-button" || this.localName === "li") return {width:20};
18
+ }
19
+ return original.call(this);
20
+ };
21
+ return () => { HTMLElement.prototype.getBoundingClientRect = original; };
22
+ }
23
+
9
24
  function dispatchPointerEvent(target, type, options = {}) {
10
25
  const event = new Event(type, {
11
26
  bubbles: true,
@@ -35,6 +50,9 @@ function dispatchPointerEvent(target, type, options = {}) {
35
50
 
36
51
  function mockScrollableElement(element, { clientWidth, scrollWidth, scrollLeft = 0 }) {
37
52
  let currentScrollLeft = scrollLeft;
53
+ // Synthetic events have no browser-owned active pointer to capture.
54
+ element.setPointerCapture = () => {};
55
+ element.releasePointerCapture = () => {};
38
56
 
39
57
  Object.defineProperty(element, "clientWidth", {
40
58
  configurable: true,
@@ -447,6 +465,7 @@ describe("Datatable drag scroll", function () {
447
465
  });
448
466
 
449
467
  it("defers column bar resize updates to the next animation frame", async function () {
468
+ let restoreGeometry = () => {};
450
469
  const OriginalResizeObserver = window.ResizeObserver;
451
470
  const originalGlobalResizeObserver = globalThis.ResizeObserver;
452
471
  const originalRequestAnimationFrame = window.requestAnimationFrame;
@@ -468,6 +487,7 @@ describe("Datatable drag scroll", function () {
468
487
  const mocks = document.getElementById("mocks");
469
488
  const wrapper = document.createElement("div");
470
489
  let parentWidth = 400;
490
+ restoreGeometry = installColumnBarGeometry(() => parentWidth);
471
491
 
472
492
  Object.defineProperty(wrapper, "getBoundingClientRect", {
473
493
  configurable: true,
@@ -499,6 +519,7 @@ describe("Datatable drag scroll", function () {
499
519
  "[data-monster-role=dots]",
500
520
  );
501
521
  const dots = Array.from(dotsContainer.querySelectorAll("li"));
522
+ dots.forEach(dot => { dot.style.margin = "0"; });
502
523
 
503
524
  expect(control).to.exist;
504
525
  expect(settingsButton).to.exist;
@@ -519,7 +540,7 @@ describe("Datatable drag scroll", function () {
519
540
  });
520
541
  });
521
542
 
522
- await new Promise((resolve) => setTimeout(resolve, 0));
543
+ await new Promise((resolve) => setTimeout(resolve, 30));
523
544
 
524
545
  expect(
525
546
  dotsContainer.querySelector(".dots-overflow-indicator"),
@@ -551,6 +572,7 @@ describe("Datatable drag scroll", function () {
551
572
  dotsContainer.querySelector(".dots-overflow-indicator"),
552
573
  ).to.exist;
553
574
  } finally {
575
+ restoreGeometry();
554
576
  window.ResizeObserver = OriginalResizeObserver;
555
577
  globalThis.ResizeObserver = originalGlobalResizeObserver;
556
578
  window.requestAnimationFrame = originalRequestAnimationFrame;
@@ -559,6 +581,7 @@ describe("Datatable drag scroll", function () {
559
581
  });
560
582
 
561
583
  it("coalesces column bar dot updates and ignores overflow indicator mutations", async function () {
584
+ let restoreGeometry = () => {};
562
585
  const OriginalResizeObserver = window.ResizeObserver;
563
586
  const originalGlobalResizeObserver = globalThis.ResizeObserver;
564
587
  const originalRequestAnimationFrame = window.requestAnimationFrame;
@@ -580,6 +603,7 @@ describe("Datatable drag scroll", function () {
580
603
  const mocks = document.getElementById("mocks");
581
604
  const wrapper = document.createElement("div");
582
605
  let parentWidth = 80;
606
+ restoreGeometry = installColumnBarGeometry(() => parentWidth);
583
607
 
584
608
  Object.defineProperty(wrapper, "getBoundingClientRect", {
585
609
  configurable: true,
@@ -612,6 +636,7 @@ describe("Datatable drag scroll", function () {
612
636
  "[data-monster-role=dots]",
613
637
  );
614
638
  const dots = Array.from(dotsContainer.querySelectorAll("li"));
639
+ dots.forEach(dot => { dot.style.margin = "0"; });
615
640
 
616
641
  Object.defineProperty(control, "getBoundingClientRect", {
617
642
  configurable: true,
@@ -628,6 +653,7 @@ describe("Datatable drag scroll", function () {
628
653
  });
629
654
  });
630
655
 
656
+ await new Promise((resolve) => setTimeout(resolve, 30));
631
657
  return { columnBar, control, dotsContainer };
632
658
  };
633
659
 
@@ -669,6 +695,7 @@ describe("Datatable drag scroll", function () {
669
695
 
670
696
  expect(scheduledCallbacks.length).to.equal(0);
671
697
  } finally {
698
+ restoreGeometry();
672
699
  window.ResizeObserver = OriginalResizeObserver;
673
700
  globalThis.ResizeObserver = originalGlobalResizeObserver;
674
701
  window.requestAnimationFrame = originalRequestAnimationFrame;
@@ -104,6 +104,9 @@ describe("ButtonBar", function () {
104
104
  "Weitere Aktionen",
105
105
  );
106
106
 
107
+ // The empty fixture has no overflow; expose the switch for the focus check.
108
+ switchButton.hidden = false;
109
+ switchButton.classList.remove("hidden");
107
110
  bar.showDialog();
108
111
  expect(switchButton.getAttribute("aria-expanded")).to.equal("true");
109
112
  switchButton.dispatchEvent(
@@ -403,7 +406,7 @@ describe("ButtonBar", function () {
403
406
  it("should allow themes to space main and overflow button groups", function () {
404
407
  const cssText = ButtonBar.getCSSStyleSheet()
405
408
  .flatMap((styleSheet) => Array.from(styleSheet.cssRules))
406
- .map((rule) => rule.cssText)
409
+ .map((rule) => rule.cssText.replace(/="([\w-]+)"/g, "=$1").replace(/>\s+\[/g, ">["))
407
410
  .join("\n");
408
411
 
409
412
  expect(cssText).to.contain("slot[name=popper]");
@@ -424,7 +427,7 @@ describe("ButtonBar", function () {
424
427
  it("should stack a nested button bar inside a parent overflow item", function () {
425
428
  const cssText = ButtonBar.getCSSStyleSheet()
426
429
  .flatMap((styleSheet) => Array.from(styleSheet.cssRules))
427
- .map((rule) => rule.cssText)
430
+ .map((rule) => rule.cssText.replace(/="([\w-]+)"/g, "=$1").replace(/>\s+\[/g, ">["))
428
431
  .join("\n");
429
432
 
430
433
  expect(cssText).to.match(
@@ -517,11 +520,11 @@ describe("ButtonBar", function () {
517
520
 
518
521
  expect(buttons[1].style.marginLeft).to.equal("");
519
522
  expect(buttons[0].style.borderRadius).to.equal("8px");
520
- expect(buttons[0].style.borderTopRightRadius).to.equal("");
521
- expect(buttons[0].style.borderBottomRightRadius).to.equal("");
523
+ expect(buttons[0].style.borderTopRightRadius || buttons[0].style.borderRadius).to.equal("8px");
524
+ expect(buttons[0].style.borderBottomRightRadius || buttons[0].style.borderRadius).to.equal("8px");
522
525
  expect(buttons[1].style.borderRadius).to.equal("8px");
523
- expect(buttons[1].style.borderTopLeftRadius).to.equal("");
524
- expect(buttons[1].style.borderBottomLeftRadius).to.equal("");
526
+ expect(buttons[1].style.borderTopLeftRadius || buttons[1].style.borderRadius).to.equal("8px");
527
+ expect(buttons[1].style.borderBottomLeftRadius || buttons[1].style.borderRadius).to.equal("8px");
525
528
  } finally {
526
529
  window.requestAnimationFrame = originalRequestAnimationFrame;
527
530
  globalThis.requestAnimationFrame = originalGlobalRequestAnimationFrame;
@@ -150,7 +150,7 @@ describe('ConfirmButton', function () {
150
150
 
151
151
  const cssText = ConfirmButton.getCSSStyleSheet()
152
152
  .flatMap((styleSheet) => Array.from(styleSheet.cssRules))
153
- .map((rule) => rule.cssText)
153
+ .map((rule) => rule.cssText.replace(/="([\w-]+)"/g, "=$1").replace(/>\s+\[/g, ">["))
154
154
  .join("\n");
155
155
 
156
156
  expect(cssText).to.contain('div[data-monster-role=decision]');
@@ -162,14 +162,14 @@ describe('ConfirmButton', function () {
162
162
 
163
163
  const cssText = ConfirmButton.getCSSStyleSheet()
164
164
  .flatMap((styleSheet) => Array.from(styleSheet.cssRules))
165
- .map((rule) => rule.cssText)
165
+ .map((rule) => rule.cssText.replace(/="([\w-]+)"/g, "=$1").replace(/>\s+\[/g, ">["))
166
166
  .join("\n");
167
167
 
168
168
  expect(cssText).to.contain('div[data-monster-role=decision]');
169
169
  expect(cssText).to.contain('max-width: 100%');
170
170
  expect(cssText).to.contain('min-width: 0px');
171
171
  expect(cssText).to.contain('flex-wrap: wrap');
172
- expect(cssText).to.contain('div[data-monster-role=decision]>*');
172
+ expect(cssText.replace(/\s*>\s*/g, '>')).to.contain('div[data-monster-role=decision]>*');
173
173
  expect(cssText).to.contain('flex: 1 1 max-content');
174
174
 
175
175
  });
@@ -178,7 +178,7 @@ describe('ConfirmButton', function () {
178
178
 
179
179
  const cssText = ConfirmButton.getCSSStyleSheet()
180
180
  .flatMap((styleSheet) => Array.from(styleSheet.cssRules))
181
- .map((rule) => rule.cssText)
181
+ .map((rule) => rule.cssText.replace(/="([\w-]+)"/g, "=$1").replace(/>\s+\[/g, ">["))
182
182
  .join("\n");
183
183
 
184
184
  expect(cssText).to.contain(':host(monster-confirm-button)');