@schukai/monster 4.148.7 → 4.148.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/package.json +1 -1
- package/source/components/form/control-bar.mjs +212 -177
- package/source/dom/customelement.mjs +105 -40
- package/source/dom/updater.mjs +381 -119
- package/test/cases/components/form/control-bar.mjs +218 -10
- package/test/cases/dom/customelement.mjs +136 -1
- package/test/cases/dom/updater.mjs +174 -0
|
@@ -1439,7 +1439,7 @@ describe("ControlBar", function () {
|
|
|
1439
1439
|
}
|
|
1440
1440
|
});
|
|
1441
1441
|
|
|
1442
|
-
it("should
|
|
1442
|
+
it("should produce the same overflow layout across repeated switch-width threshold crossings", async function () {
|
|
1443
1443
|
const OriginalResizeObserver = window.ResizeObserver;
|
|
1444
1444
|
const originalGlobalResizeObserver = globalThis.ResizeObserver;
|
|
1445
1445
|
const originalRequestAnimationFrame = window.requestAnimationFrame;
|
|
@@ -1524,7 +1524,11 @@ describe("ControlBar", function () {
|
|
|
1524
1524
|
);
|
|
1525
1525
|
Object.defineProperty(switchButton, "offsetWidth", {
|
|
1526
1526
|
configurable: true,
|
|
1527
|
-
|
|
1527
|
+
get: () =>
|
|
1528
|
+
switchButton.hasAttribute("hidden") ||
|
|
1529
|
+
switchButton.classList.contains("hidden")
|
|
1530
|
+
? 0
|
|
1531
|
+
: 20,
|
|
1528
1532
|
});
|
|
1529
1533
|
|
|
1530
1534
|
await flushFrames();
|
|
@@ -1538,25 +1542,25 @@ describe("ControlBar", function () {
|
|
|
1538
1542
|
wrapperWidth = 155;
|
|
1539
1543
|
resizeObserver.triggerResize([{ target: wrapper }]);
|
|
1540
1544
|
await flushFrames();
|
|
1541
|
-
expect(switchButton.hasAttribute("hidden")).to.be.
|
|
1542
|
-
expect(document.getElementById("threshold-three").
|
|
1543
|
-
.
|
|
1545
|
+
expect(switchButton.hasAttribute("hidden")).to.be.true;
|
|
1546
|
+
expect(document.getElementById("threshold-three").hasAttribute("slot")).to
|
|
1547
|
+
.be.false;
|
|
1544
1548
|
|
|
1545
|
-
wrapperWidth =
|
|
1549
|
+
wrapperWidth = 145;
|
|
1546
1550
|
resizeObserver.triggerResize([{ target: wrapper }]);
|
|
1547
1551
|
await flushFrames();
|
|
1548
1552
|
expect(switchButton.hasAttribute("hidden")).to.be.false;
|
|
1549
1553
|
expect(document.getElementById("threshold-three").getAttribute("slot")).to
|
|
1550
1554
|
.equal("popper");
|
|
1551
1555
|
|
|
1552
|
-
wrapperWidth =
|
|
1556
|
+
wrapperWidth = 155;
|
|
1553
1557
|
resizeObserver.triggerResize([{ target: wrapper }]);
|
|
1554
1558
|
await flushFrames();
|
|
1555
1559
|
expect(switchButton.hasAttribute("hidden")).to.be.true;
|
|
1556
1560
|
expect(document.getElementById("threshold-three").hasAttribute("slot")).to
|
|
1557
1561
|
.be.false;
|
|
1558
1562
|
|
|
1559
|
-
wrapperWidth =
|
|
1563
|
+
wrapperWidth = 145;
|
|
1560
1564
|
resizeObserver.triggerResize([{ target: wrapper }]);
|
|
1561
1565
|
await flushFrames();
|
|
1562
1566
|
expect(switchButton.hasAttribute("hidden")).to.be.false;
|
|
@@ -1570,6 +1574,201 @@ describe("ControlBar", function () {
|
|
|
1570
1574
|
}
|
|
1571
1575
|
});
|
|
1572
1576
|
|
|
1577
|
+
it("should not count rendered padding and borders twice when controls fit", async function () {
|
|
1578
|
+
const originalRequestAnimationFrame = window.requestAnimationFrame;
|
|
1579
|
+
const originalGlobalRequestAnimationFrame = globalThis.requestAnimationFrame;
|
|
1580
|
+
const scheduledCallbacks = [];
|
|
1581
|
+
const flushFrames = async () => {
|
|
1582
|
+
while (scheduledCallbacks.length > 0) {
|
|
1583
|
+
scheduledCallbacks.shift()();
|
|
1584
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1585
|
+
}
|
|
1586
|
+
};
|
|
1587
|
+
|
|
1588
|
+
try {
|
|
1589
|
+
window.requestAnimationFrame = (callback) => {
|
|
1590
|
+
scheduledCallbacks.push(callback);
|
|
1591
|
+
return scheduledCallbacks.length;
|
|
1592
|
+
};
|
|
1593
|
+
globalThis.requestAnimationFrame = window.requestAnimationFrame;
|
|
1594
|
+
|
|
1595
|
+
const mocks = document.getElementById("mocks");
|
|
1596
|
+
mocks.innerHTML = `
|
|
1597
|
+
<div id="border-box-wrapper">
|
|
1598
|
+
<monster-control-bar id="border-box-bar">
|
|
1599
|
+
<button id="border-box-one" style="padding: 10px; border: 2px solid">One</button>
|
|
1600
|
+
<button id="border-box-two" style="padding: 10px; border: 2px solid">Two</button>
|
|
1601
|
+
</monster-control-bar>
|
|
1602
|
+
</div>
|
|
1603
|
+
`;
|
|
1604
|
+
|
|
1605
|
+
const wrapper = document.getElementById("border-box-wrapper");
|
|
1606
|
+
const bar = document.getElementById("border-box-bar");
|
|
1607
|
+
const buttons = [
|
|
1608
|
+
document.getElementById("border-box-one"),
|
|
1609
|
+
document.getElementById("border-box-two"),
|
|
1610
|
+
];
|
|
1611
|
+
|
|
1612
|
+
wrapper.style.boxSizing = "border-box";
|
|
1613
|
+
Object.defineProperty(wrapper, "clientWidth", {
|
|
1614
|
+
configurable: true,
|
|
1615
|
+
value: 130,
|
|
1616
|
+
});
|
|
1617
|
+
|
|
1618
|
+
for (const button of buttons) {
|
|
1619
|
+
Object.defineProperty(button, "offsetWidth", {
|
|
1620
|
+
configurable: true,
|
|
1621
|
+
value: 60,
|
|
1622
|
+
});
|
|
1623
|
+
Object.defineProperty(button, "offsetHeight", {
|
|
1624
|
+
configurable: true,
|
|
1625
|
+
value: 30,
|
|
1626
|
+
});
|
|
1627
|
+
button.getBoundingClientRect = () => ({
|
|
1628
|
+
width: 60,
|
|
1629
|
+
height: 30,
|
|
1630
|
+
top: 0,
|
|
1631
|
+
right: 60,
|
|
1632
|
+
bottom: 30,
|
|
1633
|
+
left: 0,
|
|
1634
|
+
x: 0,
|
|
1635
|
+
y: 0,
|
|
1636
|
+
toJSON: () => {},
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
const switchButton = bar.shadowRoot.querySelector(
|
|
1641
|
+
'[data-monster-role="switch"]',
|
|
1642
|
+
);
|
|
1643
|
+
Object.defineProperty(switchButton, "offsetWidth", {
|
|
1644
|
+
configurable: true,
|
|
1645
|
+
get: () =>
|
|
1646
|
+
switchButton.hasAttribute("hidden") ||
|
|
1647
|
+
switchButton.classList.contains("hidden")
|
|
1648
|
+
? 0
|
|
1649
|
+
: 20,
|
|
1650
|
+
});
|
|
1651
|
+
|
|
1652
|
+
await flushFrames();
|
|
1653
|
+
|
|
1654
|
+
expect(switchButton.hasAttribute("hidden")).to.be.true;
|
|
1655
|
+
expect(buttons.every((button) => !button.hasAttribute("slot"))).to.be.true;
|
|
1656
|
+
} finally {
|
|
1657
|
+
window.requestAnimationFrame = originalRequestAnimationFrame;
|
|
1658
|
+
globalThis.requestAnimationFrame = originalGlobalRequestAnimationFrame;
|
|
1659
|
+
}
|
|
1660
|
+
});
|
|
1661
|
+
|
|
1662
|
+
it("should recalculate overflow when a visible control width changes", async function () {
|
|
1663
|
+
const OriginalResizeObserver = window.ResizeObserver;
|
|
1664
|
+
const originalGlobalResizeObserver = globalThis.ResizeObserver;
|
|
1665
|
+
const originalRequestAnimationFrame = window.requestAnimationFrame;
|
|
1666
|
+
const originalGlobalRequestAnimationFrame = globalThis.requestAnimationFrame;
|
|
1667
|
+
|
|
1668
|
+
class TrackingResizeObserver extends ResizeObserverMock {
|
|
1669
|
+
static instances = [];
|
|
1670
|
+
|
|
1671
|
+
constructor(callback) {
|
|
1672
|
+
super(callback);
|
|
1673
|
+
TrackingResizeObserver.instances.push(this);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
const scheduledCallbacks = [];
|
|
1678
|
+
const flushFrames = async () => {
|
|
1679
|
+
while (scheduledCallbacks.length > 0) {
|
|
1680
|
+
scheduledCallbacks.shift()();
|
|
1681
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1682
|
+
}
|
|
1683
|
+
};
|
|
1684
|
+
|
|
1685
|
+
try {
|
|
1686
|
+
window.ResizeObserver = TrackingResizeObserver;
|
|
1687
|
+
globalThis.ResizeObserver = TrackingResizeObserver;
|
|
1688
|
+
window.requestAnimationFrame = (callback) => {
|
|
1689
|
+
scheduledCallbacks.push(callback);
|
|
1690
|
+
return scheduledCallbacks.length;
|
|
1691
|
+
};
|
|
1692
|
+
globalThis.requestAnimationFrame = window.requestAnimationFrame;
|
|
1693
|
+
|
|
1694
|
+
const mocks = document.getElementById("mocks");
|
|
1695
|
+
mocks.innerHTML = `
|
|
1696
|
+
<div id="item-resize-wrapper">
|
|
1697
|
+
<monster-control-bar id="item-resize-bar">
|
|
1698
|
+
<button id="item-resize-one">One</button>
|
|
1699
|
+
<button id="item-resize-two">Two</button>
|
|
1700
|
+
</monster-control-bar>
|
|
1701
|
+
</div>
|
|
1702
|
+
`;
|
|
1703
|
+
|
|
1704
|
+
const wrapper = document.getElementById("item-resize-wrapper");
|
|
1705
|
+
const bar = document.getElementById("item-resize-bar");
|
|
1706
|
+
const buttons = [
|
|
1707
|
+
document.getElementById("item-resize-one"),
|
|
1708
|
+
document.getElementById("item-resize-two"),
|
|
1709
|
+
];
|
|
1710
|
+
const widths = [60, 60];
|
|
1711
|
+
|
|
1712
|
+
wrapper.style.boxSizing = "border-box";
|
|
1713
|
+
Object.defineProperty(wrapper, "clientWidth", {
|
|
1714
|
+
configurable: true,
|
|
1715
|
+
value: 160,
|
|
1716
|
+
});
|
|
1717
|
+
|
|
1718
|
+
buttons.forEach((button, index) => {
|
|
1719
|
+
Object.defineProperty(button, "offsetWidth", {
|
|
1720
|
+
configurable: true,
|
|
1721
|
+
get: () => widths[index],
|
|
1722
|
+
});
|
|
1723
|
+
Object.defineProperty(button, "offsetHeight", {
|
|
1724
|
+
configurable: true,
|
|
1725
|
+
value: 30,
|
|
1726
|
+
});
|
|
1727
|
+
button.getBoundingClientRect = () => ({
|
|
1728
|
+
width: widths[index],
|
|
1729
|
+
height: 30,
|
|
1730
|
+
top: 0,
|
|
1731
|
+
right: widths[index],
|
|
1732
|
+
bottom: 30,
|
|
1733
|
+
left: 0,
|
|
1734
|
+
x: 0,
|
|
1735
|
+
y: 0,
|
|
1736
|
+
toJSON: () => {},
|
|
1737
|
+
});
|
|
1738
|
+
});
|
|
1739
|
+
|
|
1740
|
+
const switchButton = bar.shadowRoot.querySelector(
|
|
1741
|
+
'[data-monster-role="switch"]',
|
|
1742
|
+
);
|
|
1743
|
+
Object.defineProperty(switchButton, "offsetWidth", {
|
|
1744
|
+
configurable: true,
|
|
1745
|
+
value: 20,
|
|
1746
|
+
});
|
|
1747
|
+
|
|
1748
|
+
await flushFrames();
|
|
1749
|
+
expect(buttons.every((button) => !button.hasAttribute("slot"))).to.be.true;
|
|
1750
|
+
|
|
1751
|
+
const resizeObserver = TrackingResizeObserver.instances.find((observer) =>
|
|
1752
|
+
observer.elements.includes(wrapper),
|
|
1753
|
+
);
|
|
1754
|
+
expect(resizeObserver).to.exist;
|
|
1755
|
+
|
|
1756
|
+
widths[0] = 110;
|
|
1757
|
+
resizeObserver.triggerResize([{ target: buttons[0] }]);
|
|
1758
|
+
expect(scheduledCallbacks.length).to.equal(1);
|
|
1759
|
+
await flushFrames();
|
|
1760
|
+
|
|
1761
|
+
expect(buttons[0].hasAttribute("slot")).to.be.false;
|
|
1762
|
+
expect(buttons[1].getAttribute("slot")).to.equal("popper");
|
|
1763
|
+
expect(switchButton.hasAttribute("hidden")).to.be.false;
|
|
1764
|
+
} finally {
|
|
1765
|
+
window.ResizeObserver = OriginalResizeObserver;
|
|
1766
|
+
globalThis.ResizeObserver = originalGlobalResizeObserver;
|
|
1767
|
+
window.requestAnimationFrame = originalRequestAnimationFrame;
|
|
1768
|
+
globalThis.requestAnimationFrame = originalGlobalRequestAnimationFrame;
|
|
1769
|
+
}
|
|
1770
|
+
});
|
|
1771
|
+
|
|
1573
1772
|
it("should move spacers with adjacent overflow controls", async function () {
|
|
1574
1773
|
const OriginalResizeObserver = window.ResizeObserver;
|
|
1575
1774
|
const originalGlobalResizeObserver = globalThis.ResizeObserver;
|
|
@@ -1787,7 +1986,7 @@ describe("ControlBar", function () {
|
|
|
1787
1986
|
}
|
|
1788
1987
|
});
|
|
1789
1988
|
|
|
1790
|
-
it("should
|
|
1989
|
+
it("should restore the maximal fitting prefix regardless of resize direction", async function () {
|
|
1791
1990
|
const OriginalResizeObserver = window.ResizeObserver;
|
|
1792
1991
|
const originalGlobalResizeObserver = globalThis.ResizeObserver;
|
|
1793
1992
|
const originalRequestAnimationFrame = window.requestAnimationFrame;
|
|
@@ -1890,13 +2089,22 @@ describe("ControlBar", function () {
|
|
|
1890
2089
|
wrapperWidth = 190;
|
|
1891
2090
|
resizeObserver.triggerResize([{ target: wrapper }]);
|
|
1892
2091
|
await flushFrames();
|
|
2092
|
+
expect(switchButton.hasAttribute("hidden")).to.be.true;
|
|
2093
|
+
expect(document.getElementById("sticky-overflow-wide").hasAttribute("slot"))
|
|
2094
|
+
.to.be.false;
|
|
2095
|
+
expect(document.getElementById("sticky-overflow-small").hasAttribute("slot"))
|
|
2096
|
+
.to.be.false;
|
|
2097
|
+
|
|
2098
|
+
wrapperWidth = 90;
|
|
2099
|
+
resizeObserver.triggerResize([{ target: wrapper }]);
|
|
2100
|
+
await flushFrames();
|
|
1893
2101
|
expect(switchButton.hasAttribute("hidden")).to.be.false;
|
|
1894
2102
|
expect(document.getElementById("sticky-overflow-wide").getAttribute("slot"))
|
|
1895
2103
|
.to.equal("popper");
|
|
1896
2104
|
expect(document.getElementById("sticky-overflow-small").getAttribute("slot"))
|
|
1897
2105
|
.to.equal("popper");
|
|
1898
2106
|
|
|
1899
|
-
wrapperWidth =
|
|
2107
|
+
wrapperWidth = 190;
|
|
1900
2108
|
resizeObserver.triggerResize([{ target: wrapper }]);
|
|
1901
2109
|
await flushFrames();
|
|
1902
2110
|
expect(switchButton.hasAttribute("hidden")).to.be.true;
|
|
@@ -4,6 +4,7 @@ import * as chai from 'chai';
|
|
|
4
4
|
import {internalSymbol} from "../../../source/constants.mjs";
|
|
5
5
|
import {customElementUpdaterLinkSymbol} from "../../../source/dom/constants.mjs";
|
|
6
6
|
import {getDocument} from "../../../source/dom/util.mjs";
|
|
7
|
+
import {Observer} from "../../../source/types/observer.mjs";
|
|
7
8
|
import {ProxyObserver} from "../../../source/types/proxyobserver.mjs";
|
|
8
9
|
import {chaiDom} from "../../util/chai-dom.mjs";
|
|
9
10
|
import {initJSDOM} from "../../util/jsdom.mjs";
|
|
@@ -28,7 +29,7 @@ const updaterSymbolSymbol = Symbol.for(updaterSymbolKey);
|
|
|
28
29
|
|
|
29
30
|
describe('DOM', function () {
|
|
30
31
|
|
|
31
|
-
let CustomElement, registerCustomElement, TestComponent, document, TestComponent2, TestStateComponent, TestMutationComponent, HiddenStateClass, assignUpdaterToElement,
|
|
32
|
+
let CustomElement, registerCustomElement, updaterTransformerMethodsSymbol, TestComponent, document, TestComponent2, TestStateComponent, TestMutationComponent, HiddenStateClass, assignUpdaterToElement,
|
|
32
33
|
addObjectWithUpdaterToElement;
|
|
33
34
|
|
|
34
35
|
describe("assignUpdaterToElement", function () {
|
|
@@ -154,6 +155,7 @@ describe('DOM', function () {
|
|
|
154
155
|
try {
|
|
155
156
|
CustomElement = m['CustomElement'];
|
|
156
157
|
registerCustomElement = m['registerCustomElement'];
|
|
158
|
+
updaterTransformerMethodsSymbol = m['updaterTransformerMethodsSymbol'];
|
|
157
159
|
TestComponent = class extends CustomElement {
|
|
158
160
|
static getTag() {
|
|
159
161
|
return "monster-testclass"
|
|
@@ -510,6 +512,81 @@ describe('DOM', function () {
|
|
|
510
512
|
}, 20);
|
|
511
513
|
}, 10);
|
|
512
514
|
});
|
|
515
|
+
|
|
516
|
+
it('should synchronize option arrays without unchanged proxy churn issue #517', async function () {
|
|
517
|
+
const htmlTAG = 'monster-testclass-array-sync-517';
|
|
518
|
+
|
|
519
|
+
if (!customElements.get(htmlTAG)) {
|
|
520
|
+
class ArraySyncComponent extends CustomElement {
|
|
521
|
+
static getTag() {
|
|
522
|
+
return htmlTAG;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
get defaults() {
|
|
526
|
+
return Object.assign({}, super.defaults, {
|
|
527
|
+
items: Array.from({length: 100}, (_, index) => `item-${index}`),
|
|
528
|
+
unrelated: 0,
|
|
529
|
+
templates: {
|
|
530
|
+
main: '<span id="array-sync-value" data-monster-replace="path:items | call:joinItems"></span><span id="array-sync-value-second" data-monster-replace="path:items | call:joinItems"></span>',
|
|
531
|
+
},
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
[updaterTransformerMethodsSymbol]() {
|
|
536
|
+
return {
|
|
537
|
+
joinItems: (items) => items.join(','),
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
registerCustomElement(ArraySyncComponent);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const element = document.createElement(htmlTAG);
|
|
546
|
+
document.getElementById('test1').appendChild(element);
|
|
547
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
548
|
+
|
|
549
|
+
const updaterGroups = element[customElementUpdaterLinkSymbol];
|
|
550
|
+
let linkedUpdater = null;
|
|
551
|
+
for (const group of updaterGroups) {
|
|
552
|
+
for (const updater of group) {
|
|
553
|
+
linkedUpdater = updater;
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
if (linkedUpdater) break;
|
|
557
|
+
}
|
|
558
|
+
expect(linkedUpdater).to.exist;
|
|
559
|
+
|
|
560
|
+
let notificationCount = 0;
|
|
561
|
+
class CountingObserver extends Observer {
|
|
562
|
+
update(subject) {
|
|
563
|
+
notificationCount++;
|
|
564
|
+
return super.update(subject);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
const observer = new CountingObserver(() => {});
|
|
568
|
+
linkedUpdater[internalSymbol].subject.attachObserver(observer);
|
|
569
|
+
|
|
570
|
+
try {
|
|
571
|
+
element.setOption('unrelated', 1);
|
|
572
|
+
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
573
|
+
expect(notificationCount).to.be.at.most(3);
|
|
574
|
+
|
|
575
|
+
notificationCount = 0;
|
|
576
|
+
element.setOption('items.50', 'changed');
|
|
577
|
+
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
578
|
+
|
|
579
|
+
expect(notificationCount).to.be.at.most(3);
|
|
580
|
+
expect(
|
|
581
|
+
element.shadowRoot.querySelector('#array-sync-value').textContent,
|
|
582
|
+
).to.contain('changed');
|
|
583
|
+
expect(
|
|
584
|
+
element.shadowRoot.querySelector('#array-sync-value-second').textContent,
|
|
585
|
+
).to.contain('changed');
|
|
586
|
+
} finally {
|
|
587
|
+
linkedUpdater[internalSymbol].subject.detachObserver(observer);
|
|
588
|
+
}
|
|
589
|
+
});
|
|
513
590
|
})
|
|
514
591
|
|
|
515
592
|
describe('setOptions()', function () {
|
|
@@ -860,6 +937,64 @@ describe('DOM', function () {
|
|
|
860
937
|
})
|
|
861
938
|
|
|
862
939
|
describe('mutation observer lifecycle', function () {
|
|
940
|
+
it('should coalesce child mutations without a refresh loop issue #517', async function () {
|
|
941
|
+
const htmlTAG = 'monster-testclass-mutation-loop-517';
|
|
942
|
+
|
|
943
|
+
if (!customElements.get(htmlTAG)) {
|
|
944
|
+
class MutationLoopComponent extends CustomElement {
|
|
945
|
+
static getTag() {
|
|
946
|
+
return htmlTAG;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
get defaults() {
|
|
950
|
+
return Object.assign({}, super.defaults, {
|
|
951
|
+
shadowMode: false,
|
|
952
|
+
features: {
|
|
953
|
+
mutationObserver: true,
|
|
954
|
+
},
|
|
955
|
+
templates: {
|
|
956
|
+
main: '<span data-monster-replace="static:stable"></span>',
|
|
957
|
+
},
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
registerCustomElement(MutationLoopComponent);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const element = document.createElement(htmlTAG);
|
|
966
|
+
document.getElementById('test1').appendChild(element);
|
|
967
|
+
await new Promise((resolve) => setTimeout(resolve, 120));
|
|
968
|
+
|
|
969
|
+
const updaterGroups = element[customElementUpdaterLinkSymbol];
|
|
970
|
+
let linkedUpdater = null;
|
|
971
|
+
for (const group of updaterGroups) {
|
|
972
|
+
for (const updater of group) {
|
|
973
|
+
linkedUpdater = updater;
|
|
974
|
+
break;
|
|
975
|
+
}
|
|
976
|
+
if (linkedUpdater) break;
|
|
977
|
+
}
|
|
978
|
+
expect(linkedUpdater).to.exist;
|
|
979
|
+
|
|
980
|
+
let runCount = 0;
|
|
981
|
+
const originalRun = linkedUpdater.run.bind(linkedUpdater);
|
|
982
|
+
linkedUpdater.run = function () {
|
|
983
|
+
runCount++;
|
|
984
|
+
return originalRun();
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
element.appendChild(document.createElement('i'));
|
|
988
|
+
element.appendChild(document.createElement('b'));
|
|
989
|
+
element.appendChild(document.createElement('em'));
|
|
990
|
+
await new Promise((resolve) => setTimeout(resolve, 160));
|
|
991
|
+
|
|
992
|
+
expect(runCount).to.equal(1);
|
|
993
|
+
expect(element.querySelector('[data-monster-replace]').textContent).to.equal(
|
|
994
|
+
'stable',
|
|
995
|
+
);
|
|
996
|
+
});
|
|
997
|
+
|
|
863
998
|
it('should not rerun linked updaters after disconnect', function (done) {
|
|
864
999
|
let mocks = document.getElementById('mocks');
|
|
865
1000
|
mocks.innerHTML = `<monster-testclass-mutation id="mutation-case"></monster-testclass-mutation>`;
|
|
@@ -170,6 +170,7 @@ let htmlPatchVsReplace = `
|
|
|
170
170
|
|
|
171
171
|
describe("DOM", function () {
|
|
172
172
|
let Updater = null;
|
|
173
|
+
let addObjectWithUpdaterToElement = null;
|
|
173
174
|
|
|
174
175
|
before(function (done) {
|
|
175
176
|
const options = {};
|
|
@@ -177,6 +178,7 @@ describe("DOM", function () {
|
|
|
177
178
|
import("../../../source/dom/updater.mjs")
|
|
178
179
|
.then((m) => {
|
|
179
180
|
Updater = m.Updater;
|
|
181
|
+
addObjectWithUpdaterToElement = m.addObjectWithUpdaterToElement;
|
|
180
182
|
|
|
181
183
|
if (!customElements.get("monster-test-property")) {
|
|
182
184
|
class MonsterTestProperty extends HTMLElement {
|
|
@@ -1907,6 +1909,50 @@ describe("DOM", function () {
|
|
|
1907
1909
|
});
|
|
1908
1910
|
|
|
1909
1911
|
describe("Updater reactive updates", function () {
|
|
1912
|
+
it("should enable configured batching before the initial run issue #517", async function () {
|
|
1913
|
+
const originalRun = Updater.prototype.run;
|
|
1914
|
+
const originalSetBatchUpdates = Updater.prototype.setBatchUpdates;
|
|
1915
|
+
const batchedUpdaters = new WeakSet();
|
|
1916
|
+
const batchStateAtRun = [];
|
|
1917
|
+
const symbol = Symbol("initial-batch-test");
|
|
1918
|
+
const element = document.createElement("div");
|
|
1919
|
+
element.setAttribute("data-monster-replace", "path:value");
|
|
1920
|
+
document.getElementById("mocks").appendChild(element);
|
|
1921
|
+
|
|
1922
|
+
Updater.prototype.setBatchUpdates = function (enabled) {
|
|
1923
|
+
if (enabled === true) {
|
|
1924
|
+
batchedUpdaters.add(this);
|
|
1925
|
+
}
|
|
1926
|
+
return originalSetBatchUpdates.call(this, enabled);
|
|
1927
|
+
};
|
|
1928
|
+
Updater.prototype.run = function () {
|
|
1929
|
+
batchStateAtRun.push(batchedUpdaters.has(this));
|
|
1930
|
+
return originalRun.call(this);
|
|
1931
|
+
};
|
|
1932
|
+
|
|
1933
|
+
let updaters = [];
|
|
1934
|
+
try {
|
|
1935
|
+
updaters = await Promise.all(
|
|
1936
|
+
addObjectWithUpdaterToElement.call(
|
|
1937
|
+
element,
|
|
1938
|
+
element,
|
|
1939
|
+
symbol,
|
|
1940
|
+
{ value: "initial" },
|
|
1941
|
+
{ batchUpdates: true },
|
|
1942
|
+
),
|
|
1943
|
+
);
|
|
1944
|
+
|
|
1945
|
+
expect(batchStateAtRun).to.deep.equal([true]);
|
|
1946
|
+
expect(element.textContent).to.equal("initial");
|
|
1947
|
+
} finally {
|
|
1948
|
+
Updater.prototype.run = originalRun;
|
|
1949
|
+
Updater.prototype.setBatchUpdates = originalSetBatchUpdates;
|
|
1950
|
+
for (const updater of updaters) {
|
|
1951
|
+
updater.dispose();
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
});
|
|
1955
|
+
|
|
1910
1956
|
[false, true].forEach((batchUpdates) => {
|
|
1911
1957
|
it(`should preserve microtask changes with batchUpdates=${batchUpdates} issue #496`, async function () {
|
|
1912
1958
|
let mocks = document.getElementById("mocks");
|
|
@@ -1938,6 +1984,134 @@ describe("DOM", function () {
|
|
|
1938
1984
|
});
|
|
1939
1985
|
});
|
|
1940
1986
|
|
|
1987
|
+
[false, true].forEach((batchUpdates) => {
|
|
1988
|
+
it(`should render each binding once per snapshot with batchUpdates=${batchUpdates} issue #517`, async function () {
|
|
1989
|
+
const mocks = document.getElementById("mocks");
|
|
1990
|
+
const staticBindings = Array.from(
|
|
1991
|
+
{ length: 20 },
|
|
1992
|
+
(_, index) =>
|
|
1993
|
+
`<span data-index="${index}" data-monster-replace="static:marker | call:trackStatic"></span>`,
|
|
1994
|
+
).join("");
|
|
1995
|
+
mocks.innerHTML = `
|
|
1996
|
+
<span id="snapshot-items" data-monster-replace="path:items | call:lastItem"></span>
|
|
1997
|
+
${staticBindings}
|
|
1998
|
+
<span id="snapshot-attribute" data-monster-attributes="title path:items | call:trackAttribute"></span>
|
|
1999
|
+
<input id="snapshot-property" data-monster-properties="value path:items | call:trackProperty">
|
|
2000
|
+
`;
|
|
2001
|
+
|
|
2002
|
+
const updater = new Updater(mocks, { items: [] });
|
|
2003
|
+
updater.setBatchUpdates(batchUpdates);
|
|
2004
|
+
let staticRenderCount = 0;
|
|
2005
|
+
let attributeRenderCount = 0;
|
|
2006
|
+
let propertyRenderCount = 0;
|
|
2007
|
+
updater.setCallback("trackStatic", (value) => {
|
|
2008
|
+
staticRenderCount++;
|
|
2009
|
+
return value;
|
|
2010
|
+
});
|
|
2011
|
+
updater.setCallback("lastItem", (value) => value.at(-1) ?? "");
|
|
2012
|
+
updater.setCallback("trackAttribute", (value) => {
|
|
2013
|
+
attributeRenderCount++;
|
|
2014
|
+
return value;
|
|
2015
|
+
});
|
|
2016
|
+
updater.setCallback("trackProperty", (value) => {
|
|
2017
|
+
propertyRenderCount++;
|
|
2018
|
+
return value;
|
|
2019
|
+
});
|
|
2020
|
+
|
|
2021
|
+
let innerHTMLDescriptor = null;
|
|
2022
|
+
let replacementWrites = 0;
|
|
2023
|
+
try {
|
|
2024
|
+
await updater.run();
|
|
2025
|
+
staticRenderCount = 0;
|
|
2026
|
+
attributeRenderCount = 0;
|
|
2027
|
+
propertyRenderCount = 0;
|
|
2028
|
+
|
|
2029
|
+
innerHTMLDescriptor = Object.getOwnPropertyDescriptor(
|
|
2030
|
+
Element.prototype,
|
|
2031
|
+
"innerHTML",
|
|
2032
|
+
);
|
|
2033
|
+
Object.defineProperty(Element.prototype, "innerHTML", {
|
|
2034
|
+
configurable: innerHTMLDescriptor.configurable,
|
|
2035
|
+
enumerable: innerHTMLDescriptor.enumerable,
|
|
2036
|
+
get: innerHTMLDescriptor.get,
|
|
2037
|
+
set(value) {
|
|
2038
|
+
if (
|
|
2039
|
+
mocks.contains(this) &&
|
|
2040
|
+
this.hasAttribute("data-monster-replace")
|
|
2041
|
+
) {
|
|
2042
|
+
replacementWrites++;
|
|
2043
|
+
}
|
|
2044
|
+
return innerHTMLDescriptor.set.call(this, value);
|
|
2045
|
+
},
|
|
2046
|
+
});
|
|
2047
|
+
|
|
2048
|
+
const items = updater.getSubject().items;
|
|
2049
|
+
for (let index = 0; index < 500; index++) {
|
|
2050
|
+
items.push(`new-${index}`);
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
2054
|
+
|
|
2055
|
+
expect(document.getElementById("snapshot-items").textContent).to.equal(
|
|
2056
|
+
"new-499",
|
|
2057
|
+
);
|
|
2058
|
+
expect(staticRenderCount).to.equal(20);
|
|
2059
|
+
expect(attributeRenderCount).to.equal(1);
|
|
2060
|
+
expect(propertyRenderCount).to.equal(1);
|
|
2061
|
+
expect(replacementWrites).to.equal(1);
|
|
2062
|
+
} finally {
|
|
2063
|
+
updater.dispose();
|
|
2064
|
+
if (innerHTMLDescriptor) {
|
|
2065
|
+
Object.defineProperty(
|
|
2066
|
+
Element.prototype,
|
|
2067
|
+
"innerHTML",
|
|
2068
|
+
innerHTMLDescriptor,
|
|
2069
|
+
);
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
});
|
|
2073
|
+
});
|
|
2074
|
+
|
|
2075
|
+
it("should not rewrite unchanged replacement content issue #517", async function () {
|
|
2076
|
+
const mocks = document.getElementById("mocks");
|
|
2077
|
+
mocks.innerHTML = `
|
|
2078
|
+
<span id="unchanged-replacement" data-monster-replace="static:same"></span>
|
|
2079
|
+
`;
|
|
2080
|
+
|
|
2081
|
+
const element = document.getElementById("unchanged-replacement");
|
|
2082
|
+
const descriptor = Object.getOwnPropertyDescriptor(
|
|
2083
|
+
Element.prototype,
|
|
2084
|
+
"innerHTML",
|
|
2085
|
+
);
|
|
2086
|
+
let writes = 0;
|
|
2087
|
+
|
|
2088
|
+
Object.defineProperty(Element.prototype, "innerHTML", {
|
|
2089
|
+
configurable: descriptor.configurable,
|
|
2090
|
+
enumerable: descriptor.enumerable,
|
|
2091
|
+
get: descriptor.get,
|
|
2092
|
+
set(value) {
|
|
2093
|
+
if (this === element) {
|
|
2094
|
+
writes++;
|
|
2095
|
+
}
|
|
2096
|
+
return descriptor.set.call(this, value);
|
|
2097
|
+
},
|
|
2098
|
+
});
|
|
2099
|
+
|
|
2100
|
+
const updater = new Updater(mocks, { trigger: 0 });
|
|
2101
|
+
try {
|
|
2102
|
+
await updater.run();
|
|
2103
|
+
writes = 0;
|
|
2104
|
+
updater.getSubject().trigger = 1;
|
|
2105
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
2106
|
+
|
|
2107
|
+
expect(element.textContent).to.equal("same");
|
|
2108
|
+
expect(writes).to.equal(0);
|
|
2109
|
+
} finally {
|
|
2110
|
+
updater.dispose();
|
|
2111
|
+
Object.defineProperty(Element.prototype, "innerHTML", descriptor);
|
|
2112
|
+
}
|
|
2113
|
+
});
|
|
2114
|
+
|
|
1941
2115
|
it("should update the DOM when the subject changes", function (done) {
|
|
1942
2116
|
let mocks = document.getElementById("mocks");
|
|
1943
2117
|
mocks.innerHTML = html1;
|