@schukai/monster 4.152.0 → 4.153.0

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 (34) 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/dom/resource.mjs +19 -5
  10. package/source/dom/sanitize-html.mjs +34 -29
  11. package/test/cases/components/datatable/drag-scroll.mjs +28 -1
  12. package/test/cases/components/form/button-bar.mjs +9 -6
  13. package/test/cases/components/form/confirm-button.mjs +4 -4
  14. package/test/cases/components/form/control-bar.mjs +73 -72
  15. package/test/cases/components/form/input-group.mjs +3 -1
  16. package/test/cases/components/form/password.mjs +2 -2
  17. package/test/cases/components/form/popper-button.mjs +2 -2
  18. package/test/cases/components/form/select-sort-callback.mjs +57 -0
  19. package/test/cases/components/form/select.mjs +2 -2
  20. package/test/cases/components/form/sheet.mjs +13 -1
  21. package/test/cases/components/layout/popper.mjs +3 -1
  22. package/test/cases/components/layout/tabs.mjs +4 -1
  23. package/test/cases/components/navigation/site-navigation.mjs +12 -26
  24. package/test/cases/components/navigation/table-of-content.mjs +3 -0
  25. package/test/cases/components/state/ticker-lifecycle.mjs +47 -0
  26. package/test/cases/components/time/month-calendar.mjs +100 -0
  27. package/test/cases/data/transformer.mjs +7 -0
  28. package/test/cases/dom/customcontrol.mjs +2 -6
  29. package/test/cases/dom/ready.mjs +3 -3
  30. package/test/cases/dom/resource-availability.mjs +46 -0
  31. package/test/cases/dom/resourcemanager.mjs +12 -6
  32. package/test/cases/dom/sanitize-html.mjs +71 -0
  33. package/test/cases/dom/updater.mjs +1 -1
  34. package/test/cases/util/processing.mjs +3 -3
@@ -392,74 +392,77 @@ describe("ControlBar", function () {
392
392
  const cssText = ControlBar.getCSSStyleSheet()
393
393
  .flatMap((styleSheet) => Array.from(styleSheet.cssRules))
394
394
  .map((rule) => rule.cssText)
395
- .join("\n");
396
-
397
- expect(cssText).to.contain("--monster-control-bar-height");
398
- expect(cssText).to.contain("slot[name=popper]");
399
- expect(cssText).to.contain("align-items: stretch");
400
- expect(cssText).to.contain("[data-monster-layout-alignment=center]");
401
- expect(cssText).to.contain("justify-content: center");
402
- expect(cssText).to.contain("::slotted(*)");
403
- expect(cssText).to.contain("display: flex");
404
- expect(cssText).to.contain(
395
+ .join("\n")
396
+ .replace(/="([\w-]+)"/g, "=$1");
397
+
398
+ const expectCSS = (fragment) => expect(cssText.replace(/\s/g, "")).to.contain(fragment.replace(/\s/g, ""));
399
+
400
+ expectCSS("--monster-control-bar-height");
401
+ expectCSS("slot[name=popper]");
402
+ expectCSS("align-items: stretch");
403
+ expectCSS("[data-monster-layout-alignment=center]");
404
+ expectCSS("justify-content: center");
405
+ expectCSS("::slotted(*)");
406
+ expectCSS("display: flex");
407
+ expectCSS(
405
408
  "--monster-control-min-block-size: var(--monster-control-bar-height)",
406
409
  );
407
- expect(cssText).to.contain(
410
+ expectCSS(
408
411
  "::slotted(monster-control-bar)",
409
412
  );
410
- expect(cssText).to.contain(
413
+ expectCSS(
411
414
  "--monster-control-min-block-size: inherit",
412
415
  );
413
- expect(cssText).to.contain("::slotted(form)");
414
- expect(cssText).to.contain("height: var(--monster-control-bar-height)");
415
- expect(cssText).to.contain(
416
+ expectCSS("::slotted(form)");
417
+ expectCSS("height: var(--monster-control-bar-height)");
418
+ expectCSS(
416
419
  "border-width: var(--monster-theme-control-border-width",
417
420
  );
418
- expect(cssText).to.contain("appearance: none");
419
- expect(cssText).to.contain("--monster-select-container-overflow: hidden");
420
- expect(cssText).to.contain(
421
+ expectCSS("appearance: none");
422
+ expectCSS("--monster-select-container-overflow: hidden");
423
+ expectCSS(
421
424
  "--monster-select-control-min-block-size: var(--monster-control-bar-height)",
422
425
  );
423
- expect(cssText).to.contain("::slotted(monster-button)");
424
- expect(cssText).to.contain("::slotted(monster-popper-button)");
425
- expect(cssText).to.contain("--monster-control-bar-item-flex");
426
- expect(cssText).to.contain("--monster-control-bar-slot-flex");
427
- expect(cssText).to.contain(
426
+ expectCSS("::slotted(monster-button)");
427
+ expectCSS("::slotted(monster-popper-button)");
428
+ expectCSS("--monster-control-bar-item-flex");
429
+ expectCSS("--monster-control-bar-slot-flex");
430
+ expectCSS(
428
431
  "--monster-control-bar-button-inline-size",
429
432
  );
430
- expect(cssText).to.contain("::slotted(monster-input-group)");
431
- expect(cssText).to.contain("::slotted(monster-control-bar-spacer)");
432
- expect(cssText).to.contain(
433
+ expectCSS("::slotted(monster-input-group)");
434
+ expectCSS("::slotted(monster-control-bar-spacer)");
435
+ expectCSS(
433
436
  "--monster-control-bar-spacer-line-block-size: 60%",
434
437
  );
435
- expect(cssText).to.contain(
438
+ expectCSS(
436
439
  "--monster-control-bar-spacer-line-inline-size: calc(100% - 1rem)",
437
440
  );
438
- expect(cssText).to.contain("--monster-action-menu-padding");
439
- expect(cssText).to.contain("--monster-action-menu-min-inline-size");
440
- expect(cssText).to.contain("--monster-action-menu-item-min-block-size");
441
- expect(cssText).to.contain(
441
+ expectCSS("--monster-action-menu-padding");
442
+ expectCSS("--monster-action-menu-min-inline-size");
443
+ expectCSS("--monster-action-menu-item-min-block-size");
444
+ expectCSS(
442
445
  "--monster-button-border-width: var(--monster-action-menu-item-border-width",
443
446
  );
444
- expect(cssText).to.contain(
447
+ expectCSS(
445
448
  "--monster-button-border-radius: var(--monster-action-menu-item-border-radius",
446
449
  );
447
- expect(cssText).to.contain(
450
+ expectCSS(
448
451
  "--monster-button-justify-content: var(--monster-action-menu-item-justify-content",
449
452
  );
450
- expect(cssText).to.contain(
453
+ expectCSS(
451
454
  "--monster-button-background-color: var(--monster-action-menu-item-background-color",
452
455
  );
453
- expect(cssText).to.contain(
456
+ expectCSS(
454
457
  "--monster-action-menu-item-hover-background-color",
455
458
  );
456
- expect(cssText).to.contain("--monster-action-menu-item-focus-outline-offset");
457
- expect(cssText).to.contain("--monster-button-bar-flex-direction: column");
458
- expect(cssText).to.contain("--monster-button-bar-height: auto");
459
- expect(cssText).to.contain(
459
+ expectCSS("--monster-action-menu-item-focus-outline-offset");
460
+ expectCSS("--monster-button-bar-flex-direction: column");
461
+ expectCSS("--monster-button-bar-height: auto");
462
+ expectCSS(
460
463
  ":has(>[data-monster-role=switch][hidden])",
461
464
  );
462
- expect(cssText).to.contain("height: auto !important");
465
+ expectCSS("height: auto !important");
463
466
  });
464
467
 
465
468
  it("should join adjacent borders using the smaller computed border width", async function () {
@@ -530,12 +533,12 @@ describe("ControlBar", function () {
530
533
  });
531
534
  }
532
535
 
533
- controls[0].style.borderRightWidth = "3px";
534
- controls[1].style.borderLeftWidth = "2px";
535
- controls[1].style.borderRightWidth = "2px";
536
- controls[2].style.borderLeftWidth = "2px";
537
- controls[2].style.borderRightWidth = "3px";
538
- controls[3].style.borderLeftWidth = "3px";
536
+ controls[0].style.borderRight = "3px solid";
537
+ controls[1].style.borderLeft = "2px solid";
538
+ controls[1].style.borderRight = "2px solid";
539
+ controls[2].style.borderLeft = "2px solid";
540
+ controls[2].style.borderRight = "3px solid";
541
+ controls[3].style.borderLeft = "3px solid";
539
542
 
540
543
  await flushFrames();
541
544
  await new Promise((resolve) => setTimeout(resolve, 0));
@@ -617,8 +620,8 @@ describe("ControlBar", function () {
617
620
  });
618
621
  }
619
622
 
620
- button.style.borderRightWidth = "3px";
621
- innerControl.style.borderLeftWidth = "2px";
623
+ button.style.borderRight = "3px solid";
624
+ innerControl.style.borderLeft = "2px solid";
622
625
 
623
626
  await flushFrames();
624
627
  await new Promise((resolve) => setTimeout(resolve, 0));
@@ -705,8 +708,8 @@ describe("ControlBar", function () {
705
708
  });
706
709
 
707
710
  const innerButton = control.shadowRoot?.querySelector("button");
708
- innerButton.style.borderLeftWidth = "3px";
709
- innerButton.style.borderRightWidth = "3px";
711
+ innerButton.style.borderLeft = "3px solid";
712
+ innerButton.style.borderRight = "3px solid";
710
713
  innerButton.style.borderRadius = "8px";
711
714
  }
712
715
 
@@ -720,19 +723,17 @@ describe("ControlBar", function () {
720
723
  const innerButtons = controls.map((control) =>
721
724
  control.shadowRoot?.querySelector("button"),
722
725
  );
723
- expect(innerButtons[0].style.borderRadius).to.equal("8px");
724
- expect(innerButtons[0].style.borderTopLeftRadius).to.equal("");
726
+ expect(innerButtons[0].style.borderTopLeftRadius || innerButtons[0].style.borderRadius).to.equal("8px");
725
727
  expect(innerButtons[0].style.borderTopRightRadius).to.equal("0px");
726
728
  expect(innerButtons[0].style.borderBottomRightRadius).to.equal("0px");
727
- expect(innerButtons[0].style.borderBottomLeftRadius).to.equal("");
729
+ expect(innerButtons[0].style.borderBottomLeftRadius || innerButtons[0].style.borderRadius).to.equal("8px");
728
730
  expect(innerButtons[1].style.borderTopLeftRadius).to.equal("0px");
729
731
  expect(innerButtons[1].style.borderTopRightRadius).to.equal("0px");
730
732
  expect(innerButtons[1].style.borderBottomRightRadius).to.equal("0px");
731
733
  expect(innerButtons[1].style.borderBottomLeftRadius).to.equal("0px");
732
734
  expect(innerButtons[2].style.borderTopLeftRadius).to.equal("0px");
733
- expect(innerButtons[2].style.borderRadius).to.equal("8px");
734
- expect(innerButtons[2].style.borderTopRightRadius).to.equal("");
735
- expect(innerButtons[2].style.borderBottomRightRadius).to.equal("");
735
+ expect(innerButtons[2].style.borderTopRightRadius || innerButtons[2].style.borderRadius).to.equal("8px");
736
+ expect(innerButtons[2].style.borderBottomRightRadius || innerButtons[2].style.borderRadius).to.equal("8px");
736
737
  expect(innerButtons[2].style.borderBottomLeftRadius).to.equal("0px");
737
738
  } finally {
738
739
  window.requestAnimationFrame = originalRequestAnimationFrame;
@@ -836,10 +837,10 @@ describe("ControlBar", function () {
836
837
  '[data-monster-role="button"]',
837
838
  );
838
839
 
839
- releaseInnerButton.style.borderRightWidth = "3px";
840
- cancelInnerButton.style.borderLeftWidth = "3px";
841
- cancelInnerButton.style.borderRightWidth = "2px";
842
- followUpInnerButton.style.borderLeftWidth = "2px";
840
+ releaseInnerButton.style.borderRight = "3px solid";
841
+ cancelInnerButton.style.borderLeft = "3px solid";
842
+ cancelInnerButton.style.borderRight = "2px solid";
843
+ followUpInnerButton.style.borderLeft = "2px solid";
843
844
 
844
845
  await flushFrames();
845
846
  await new Promise((resolve) => setTimeout(resolve, 0));
@@ -942,8 +943,8 @@ describe("ControlBar", function () {
942
943
  '[data-monster-role="button"]',
943
944
  );
944
945
 
945
- copyInnerButton.style.borderRightWidth = "3px";
946
- repairInnerButton.style.borderLeftWidth = "3px";
946
+ copyInnerButton.style.borderRight = "3px solid";
947
+ repairInnerButton.style.borderLeft = "3px solid";
947
948
 
948
949
  await flushFrames();
949
950
  await new Promise((resolve) => setTimeout(resolve, 0));
@@ -1020,8 +1021,8 @@ describe("ControlBar", function () {
1020
1021
  });
1021
1022
  }
1022
1023
 
1023
- left.style.borderRightWidth = "4px";
1024
- right.style.borderLeftWidth = "2px";
1024
+ left.style.borderRight = "4px solid";
1025
+ right.style.borderLeft = "2px solid";
1025
1026
 
1026
1027
  await flushFrames();
1027
1028
  await new Promise((resolve) => setTimeout(resolve, 0));
@@ -1104,8 +1105,8 @@ describe("ControlBar", function () {
1104
1105
  });
1105
1106
  }
1106
1107
 
1107
- top.style.borderBottomWidth = "2px";
1108
- bottom.style.borderTopWidth = "5px";
1108
+ top.style.borderBottom = "2px solid";
1109
+ bottom.style.borderTop = "5px solid";
1109
1110
 
1110
1111
  await flushFrames();
1111
1112
  await new Promise((resolve) => setTimeout(resolve, 0));
@@ -1218,7 +1219,7 @@ describe("ControlBar", function () {
1218
1219
  configurable: true,
1219
1220
  value: 20,
1220
1221
  });
1221
- switchButton.style.borderLeftWidth = "1px";
1222
+ switchButton.style.borderLeft = "1px solid";
1222
1223
  const controlBar = bar.shadowRoot.querySelector(
1223
1224
  '[data-monster-role="control-bar"]',
1224
1225
  );
@@ -1241,11 +1242,11 @@ describe("ControlBar", function () {
1241
1242
  expect(document.getElementById("overflow-button").getAttribute("slot")).to
1242
1243
  .equal("popper");
1243
1244
  expect(controls[1].style.borderRadius).to.equal("8px");
1244
- expect(controls[1].style.borderTopLeftRadius).to.equal("");
1245
- expect(controls[1].style.borderTopRightRadius).to.equal("");
1245
+ expect(controls[1].style.borderTopLeftRadius || controls[1].style.borderRadius).to.equal("8px");
1246
+ expect(controls[1].style.borderTopRightRadius || controls[1].style.borderRadius).to.equal("8px");
1246
1247
  expect(controls[2].style.borderRadius).to.equal("8px");
1247
- expect(controls[2].style.borderTopLeftRadius).to.equal("");
1248
- expect(controls[2].style.borderTopRightRadius).to.equal("");
1248
+ expect(controls[2].style.borderTopLeftRadius || controls[2].style.borderRadius).to.equal("8px");
1249
+ expect(controls[2].style.borderTopRightRadius || controls[2].style.borderRadius).to.equal("8px");
1249
1250
  } finally {
1250
1251
  window.ResizeObserver = OriginalResizeObserver;
1251
1252
  globalThis.ResizeObserver = originalGlobalResizeObserver;
@@ -1455,7 +1456,7 @@ describe("ControlBar", function () {
1455
1456
  class="nucleus-list-main-actions"
1456
1457
  data-monster-option-layout-alignment="right"
1457
1458
  data-monster-option-layout-stacked-alignment="left"
1458
- data-monster-option-layout-stacked-breakpoint="30rem"
1459
+ data-monster-option-layout-stacked-breakpoint="400px"
1459
1460
  >
1460
1461
  <button id="reconnect-create">Create</button>
1461
1462
  <button id="reconnect-tags">Manage tags</button>
@@ -49,7 +49,9 @@ describe("InputGroup", () => {
49
49
  expect(cssText).to.contain("var(--monster-focus-ring-width");
50
50
  expect(cssText).to.contain("var(--monster-focus-ring-offset");
51
51
  expect(cssText).to.contain("::slotted(input:focus-visible)");
52
- expect(cssText).to.contain("outline: none !important");
52
+ const outline = new CSSStyleSheet();
53
+ outline.replaceSync("input { outline: none !important; }");
54
+ expect(cssText).to.contain(outline.cssRules[0].style.cssText.trim());
53
55
  expect(cssText).to.contain("box-sizing: content-box !important");
54
56
  expect(cssText).to.contain("height: 1.1rem");
55
57
  });
@@ -40,7 +40,7 @@ describe("Password", function () {
40
40
  it("should preserve the input-group radius around the reveal control", function () {
41
41
  const cssText = Password.getCSSStyleSheet()
42
42
  .flatMap((styleSheet) => Array.from(styleSheet.cssRules))
43
- .map((rule) => rule.cssText)
43
+ .map((rule) => rule.cssText.replace(/="([\w-]+)"/g, "=$1"))
44
44
  .join("\n");
45
45
 
46
46
  expect(cssText).to.contain("monster-input-group");
@@ -51,7 +51,7 @@ describe("Password", function () {
51
51
  it("should vertically center the input and reveal icon in the shared control height", function () {
52
52
  const cssText = Password.getCSSStyleSheet()
53
53
  .flatMap((styleSheet) => Array.from(styleSheet.cssRules))
54
- .map((rule) => rule.cssText)
54
+ .map((rule) => rule.cssText.replace(/="([\w-]+)"/g, "=$1"))
55
55
  .join("\n");
56
56
 
57
57
  expect(cssText).to.contain("--monster-password-control-block-size");
@@ -34,7 +34,7 @@ describe("PopperButton", function () {
34
34
  it("should style native form controls inside popper content", function () {
35
35
  const cssText = PopperButton.getCSSStyleSheet()
36
36
  .flatMap((styleSheet) => Array.from(styleSheet.cssRules))
37
- .map((rule) => rule.cssText)
37
+ .map((rule) => rule.cssText.replace(/="([\w-]+)"/g, "=$1"))
38
38
  .join("\n")
39
39
  .replaceAll(/\s+/g, "");
40
40
 
@@ -51,7 +51,7 @@ describe("PopperButton", function () {
51
51
  it("should preserve focus indicator space inside clipped popper content", function () {
52
52
  const cssText = PopperButton.getCSSStyleSheet()
53
53
  .flatMap((styleSheet) => Array.from(styleSheet.cssRules))
54
- .map((rule) => rule.cssText)
54
+ .map((rule) => rule.cssText.replace(/="([\w-]+)"/g, "=$1"))
55
55
  .join("\n")
56
56
  .replaceAll(/\s+/g, "");
57
57
 
@@ -0,0 +1,57 @@
1
+ import { expect } from "chai";
2
+ import { initJSDOM } from "../../../util/jsdom.mjs";
3
+
4
+ describe("Select mapping sorting compatibility", () => {
5
+ before(async () => {
6
+ await initJSDOM();
7
+ await import("../../../../source/components/form/select.mjs");
8
+ });
9
+ afterEach(() => document.getElementById("mocks").replaceChildren());
10
+ for (const [name, sort, expected] of [
11
+ ["callback", (a, b) => b[1].localeCompare(a[1]), ["b", "a"]],
12
+ ["run", "run:return b[1].localeCompare(a[1]);", ["b", "a"]],
13
+ ["ascending", "call:asc", ["a", "b"]],
14
+ ["descending", "call:desc", ["b", "a"]],
15
+ ["disabled", null, ["b", "a"]],
16
+ ]) {
17
+ it(`preserves ${name} mapping`, () => {
18
+ const element = document.createElement("monster-select");
19
+ document.getElementById("mocks").append(element);
20
+ element.setOptions({
21
+ mapping: {
22
+ selector: "*",
23
+ labelTemplate: "label",
24
+ valueTemplate: "value",
25
+ sort,
26
+ },
27
+ });
28
+ element.importOptions([
29
+ { value: "b", label: "Beta" },
30
+ { value: "a", label: "Alpha" },
31
+ ]);
32
+ expect(
33
+ element.getOption("options").map((option) => option.value),
34
+ ).to.deep.equal(expected);
35
+ });
36
+ }
37
+ it("passes the owning control to callback sorters", () => {
38
+ const element = document.createElement("monster-select");
39
+ document.getElementById("mocks").append(element);
40
+ let owner;
41
+ element.setOptions({
42
+ mapping: {
43
+ labelTemplate: "label",
44
+ valueTemplate: "value",
45
+ sort: (_a, _b, control) => {
46
+ owner = control;
47
+ return 0;
48
+ },
49
+ },
50
+ });
51
+ element.importOptions([
52
+ { value: "a", label: "Alpha" },
53
+ { value: "b", label: "Beta" },
54
+ ]);
55
+ expect(owner).to.equal(element);
56
+ });
57
+ });
@@ -180,7 +180,7 @@ describe('Select', function () {
180
180
  it('should expose compact layout variables for constrained control contexts', function () {
181
181
  const cssText = SelectStyleSheet
182
182
  .cssRules
183
- ? Array.from(SelectStyleSheet.cssRules).map((rule) => rule.cssText).join("\n")
183
+ ? Array.from(SelectStyleSheet.cssRules).map((rule) => rule.cssText.replace(/="([\w-]+)"/g, "=$1").replace(/>\s+\[/g, ">[")).join("\n")
184
184
  : "";
185
185
 
186
186
  expect(cssText).to.contain("--monster-select-container-overflow");
@@ -1513,7 +1513,7 @@ describe('Select', function () {
1513
1513
 
1514
1514
  it('should keep option list scroll layout stable while overflow toggles', function () {
1515
1515
  const cssText = Array.from(SelectStyleSheet.cssRules)
1516
- .map((rule) => rule.cssText)
1516
+ .map((rule) => rule.cssText.replace(/="([\w-]+)"/g, "=$1").replace(/>\s+\[/g, ">["))
1517
1517
  .join('\n')
1518
1518
  .replace(/\s+/g, '');
1519
1519
 
@@ -98,6 +98,16 @@ describe("Sheet", function () {
98
98
  clientX: 120,
99
99
  clientY: 80,
100
100
  });
101
+ // Keep the menu in a deliberately small viewport in every DOM engine.
102
+ const wrapper = sheet.shadowRoot.querySelector('[data-monster-role="grid-wrapper"]');
103
+ Object.defineProperties(wrapper, {
104
+ clientWidth: {configurable:true,value:80},
105
+ clientHeight: {configurable:true,value:40},
106
+ });
107
+ Object.defineProperties(menu, {
108
+ offsetWidth: {configurable:true,value:160},
109
+ offsetHeight: {configurable:true,value:96},
110
+ });
101
111
 
102
112
  input.dispatchEvent(event);
103
113
 
@@ -110,7 +120,7 @@ describe("Sheet", function () {
110
120
 
111
121
  const cssText = Sheet.getCSSStyleSheet()
112
122
  .flatMap((styleSheet) => Array.from(styleSheet.cssRules || []))
113
- .map((rule) => rule.cssText)
123
+ .map((rule) => rule.cssText.replace(/="([\w-]+)"/g, "=$1").replace(/>\s+\[/g, ">["))
114
124
  .join("\n")
115
125
  .replace(/\s+/g, "");
116
126
  expect(cssText).to.contain("[part=context-menu]{position:absolute");
@@ -160,6 +170,8 @@ describe("Sheet", function () {
160
170
  sheet.addEventListener("monster-sheet-resize-column", () => {
161
171
  resizeEvents++;
162
172
  });
173
+ // Ignore lifecycle frames queued while the element was connected.
174
+ scheduledCallbacks.length = 0;
163
175
 
164
176
  const down = new Event("pointerdown", { bubbles: true });
165
177
  Object.assign(down, {
@@ -41,6 +41,8 @@ describe("Popper", function () {
41
41
  const popper = host.shadowRoot.querySelector(
42
42
  '[data-monster-role="popper"]',
43
43
  );
44
+ // Use viewport coordinates in both JSDOM and a real containing block.
45
+ host.setOption("popper.strategy", "fixed");
44
46
 
45
47
  setElementRect(control, {
46
48
  x: 100,
@@ -65,7 +67,7 @@ describe("Popper", function () {
65
67
 
66
68
  await waitForCondition(() => popper.style.transform !== "");
67
69
 
68
- expect(popper.style.transform).to.contain("translate(60px,140px)");
70
+ expect(popper.style.transform.replaceAll(" ", "")).to.contain("translate(60px,140px)");
69
71
  host.hideDialog();
70
72
  });
71
73
  });
@@ -356,7 +356,7 @@ describe("Tabs", function () {
356
356
  );
357
357
  expect(cssText).to.contain("--monster-tabs-indicator-inline-size");
358
358
  expect(cssText).to.contain("--monster-tabs-indicator-active-opacity");
359
- expect(cssText).to.contain("button:before");
359
+ expect(cssText).to.match(/button::?before/);
360
360
  expect(cssText).to.contain("prefers-reduced-motion");
361
361
  expect(cssText).to.contain(":focus-visible");
362
362
  expect(cssText).to.contain(
@@ -555,6 +555,9 @@ describe("Tabs", function () {
555
555
  const switchButton = tabs.shadowRoot.querySelector(
556
556
  '[data-monster-role="switch"]',
557
557
  );
558
+ // This fixture fits all tabs; expose the switch before testing focus.
559
+ switchButton.hidden = false;
560
+ switchButton.classList.remove("hidden");
558
561
  switchButton.click();
559
562
  expect(switchButton.getAttribute("aria-expanded")).to.equal("true");
560
563
  switchButton.dispatchEvent(
@@ -30,7 +30,7 @@ describe("SiteNavigation", function () {
30
30
  );
31
31
  });
32
32
 
33
- it("should coalesce resize layout updates", function (done) {
33
+ it("should coalesce resize layout updates", async function () {
34
34
  const OriginalResizeObserver = window.ResizeObserver;
35
35
  const originalGlobalResizeObserver = globalThis.ResizeObserver;
36
36
  const originalRequestAnimationFrame = window.requestAnimationFrame;
@@ -50,11 +50,6 @@ describe("SiteNavigation", function () {
50
50
  try {
51
51
  window.ResizeObserver = TrackingResizeObserver;
52
52
  globalThis.ResizeObserver = TrackingResizeObserver;
53
- window.requestAnimationFrame = (callback) => {
54
- scheduledCallbacks.push(callback);
55
- return scheduledCallbacks.length;
56
- };
57
- globalThis.requestAnimationFrame = window.requestAnimationFrame;
58
53
 
59
54
  const mocks = document.getElementById("mocks");
60
55
  mocks.innerHTML = `
@@ -72,33 +67,24 @@ describe("SiteNavigation", function () {
72
67
  );
73
68
  expect(resizeObserver).to.exist;
74
69
 
75
- while (scheduledCallbacks.length > 0) {
76
- scheduledCallbacks.shift()();
77
- }
70
+ // Settle initial slot/layout work before measuring resize coalescing.
71
+ await new Promise(resolve => setTimeout(resolve, 260));
72
+ window.requestAnimationFrame = (callback) => {
73
+ scheduledCallbacks.push(callback);
74
+ return scheduledCallbacks.length;
75
+ };
76
+ globalThis.requestAnimationFrame = window.requestAnimationFrame;
78
77
  resizeObserver.triggerResize([]);
79
78
  resizeObserver.triggerResize([]);
80
79
 
81
- setTimeout(() => {
82
- try {
83
- expect(scheduledCallbacks.length).to.equal(1);
84
- scheduledCallbacks.shift()();
85
- done();
86
- } catch (e) {
87
- done(e);
88
- } finally {
89
- window.ResizeObserver = OriginalResizeObserver;
90
- globalThis.ResizeObserver = originalGlobalResizeObserver;
91
- window.requestAnimationFrame = originalRequestAnimationFrame;
92
- globalThis.requestAnimationFrame =
93
- originalGlobalRequestAnimationFrame;
94
- }
95
- }, 260);
96
- } catch (e) {
80
+ await new Promise(resolve => setTimeout(resolve, 260));
81
+ expect(scheduledCallbacks.length).to.equal(1);
82
+ scheduledCallbacks.shift()();
83
+ } finally {
97
84
  window.ResizeObserver = OriginalResizeObserver;
98
85
  globalThis.ResizeObserver = originalGlobalResizeObserver;
99
86
  window.requestAnimationFrame = originalRequestAnimationFrame;
100
87
  globalThis.requestAnimationFrame = originalGlobalRequestAnimationFrame;
101
- done(e);
102
88
  }
103
89
  });
104
90
  });
@@ -73,6 +73,7 @@ describe('TableOfContent', function () {
73
73
  tableOfContent.appendChild(paragraph);
74
74
 
75
75
  Object.defineProperties(outerScroller, {
76
+ scrollTop: { configurable: true, writable: true, value: 0 },
76
77
  clientHeight: { configurable: true, value: 200 },
77
78
  scrollHeight: { configurable: true, value: 600 },
78
79
  clientWidth: { configurable: true, value: 300 },
@@ -135,6 +136,7 @@ describe('TableOfContent', function () {
135
136
  tableOfContent.appendChild(heading);
136
137
 
137
138
  Object.defineProperties(outerScroller, {
139
+ scrollTop: { configurable: true, writable: true, value: 0 },
138
140
  clientHeight: { configurable: true, value: 200 },
139
141
  scrollHeight: { configurable: true, value: 600 },
140
142
  clientWidth: { configurable: true, value: 300 },
@@ -187,6 +189,7 @@ describe('TableOfContent', function () {
187
189
  tableOfContent.appendChild(heading);
188
190
 
189
191
  Object.defineProperties(outerScroller, {
192
+ scrollTop: { configurable: true, writable: true, value: 0 },
190
193
  clientHeight: { configurable: true, value: 200 },
191
194
  scrollHeight: { configurable: true, value: 600 },
192
195
  clientWidth: { configurable: true, value: 300 },
@@ -0,0 +1,47 @@
1
+ import { expect } from "chai";
2
+ import sinon from "sinon";
3
+ import { initJSDOM } from "../../../util/jsdom.mjs";
4
+
5
+ describe("State component tickers", () => {
6
+ before(async () => {
7
+ await initJSDOM();
8
+ await import("../../../../source/components/state/log.mjs");
9
+ await import("../../../../source/components/state/thread.mjs");
10
+ });
11
+ for (const tag of ["monster-log", "monster-thread"]) {
12
+ it(`${tag} stops on removal and resumes once without losing options`, () => {
13
+ const intervals = new Map();
14
+ let sequence = 0;
15
+ const set = sinon
16
+ .stub(globalThis, "setInterval")
17
+ .callsFake((callback, delay) => {
18
+ const id = ++sequence;
19
+ intervals.set(id, { callback, delay });
20
+ return id;
21
+ });
22
+ const clear = sinon
23
+ .stub(globalThis, "clearInterval")
24
+ .callsFake((id) => intervals.delete(id));
25
+ let element;
26
+ try {
27
+ element = document.createElement(tag);
28
+ element.setOption("updateFrequency", 1234);
29
+ document.getElementById("mocks").append(element);
30
+ expect(intervals.size).to.equal(1);
31
+ expect([...intervals.values()][0].delay).to.equal(1234);
32
+ const entries = element.getOption("entries");
33
+ element.remove();
34
+ expect(intervals.size).to.equal(0);
35
+ document.getElementById("mocks").append(element);
36
+ element.connectedCallback();
37
+ expect(intervals.size).to.equal(1);
38
+ expect(element.getOption("entries")).to.equal(entries);
39
+ expect(element.getOption("updateFrequency")).to.equal(1234);
40
+ } finally {
41
+ element?.remove();
42
+ set.restore();
43
+ clear.restore();
44
+ }
45
+ });
46
+ }
47
+ });