@schukai/monster 4.153.1 → 4.154.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 (39) hide show
  1. package/package.json +1 -1
  2. package/source/components/form/message-state-button.mjs +1 -1
  3. package/source/components/form/select-link.mjs +12 -1
  4. package/source/components/state/log.mjs +3 -7
  5. package/source/components/state/thread.mjs +3 -7
  6. package/source/components/time/day.mjs +0 -19
  7. package/source/components/time/month-calendar.mjs +5 -1
  8. package/source/data/buildmap.mjs +3 -3
  9. package/source/data/datasource.mjs +3 -10
  10. package/source/data/diff.mjs +13 -12
  11. package/source/data/extend.mjs +23 -1
  12. package/source/data/pathfinder.mjs +186 -19
  13. package/source/dom/customelement.mjs +273 -30
  14. package/source/dom/updater.mjs +6 -2
  15. package/source/types/base.mjs +7 -6
  16. package/source/types/basewithoptions.mjs +1 -8
  17. package/source/types/internal.mjs +4 -10
  18. package/source/types/observerlist.mjs +9 -17
  19. package/source/types/proxyobserver.mjs +19 -2
  20. package/source/types/tokenlist.mjs +13 -8
  21. package/source/util/clone.mjs +44 -14
  22. package/test/cases/browser-bundle.mjs +75 -0
  23. package/test/cases/components/time/month-calendar.mjs +115 -0
  24. package/test/cases/data/diff.mjs +86 -0
  25. package/test/cases/data/extend.mjs +52 -0
  26. package/test/cases/data/pathfinder-resolution-baseline.mjs +322 -0
  27. package/test/cases/data/pathfinder.mjs +129 -0
  28. package/test/cases/dom/customelement-rebind-baseline.mjs +323 -0
  29. package/test/cases/dom/customelement-teardown.mjs +408 -0
  30. package/test/cases/dom/customelement.mjs +354 -0
  31. package/test/cases/dom/dimension.mjs +34 -10
  32. package/test/cases/dom/updater-remove.mjs +198 -0
  33. package/test/cases/packaging.mjs +213 -0
  34. package/test/cases/types/base.mjs +50 -0
  35. package/test/cases/types/observerlist.mjs +103 -1
  36. package/test/cases/types/proxyobserver.mjs +114 -0
  37. package/test/cases/types/regex.mjs +4 -0
  38. package/test/cases/types/tokenlist.mjs +64 -1
  39. package/test/cases/util/clone.mjs +115 -0
@@ -588,6 +588,100 @@ describe('DOM', function () {
588
588
  }
589
589
  });
590
590
 
591
+ it('should not re-render unrelated NaN option paths on every change issue #571', async function () {
592
+ const htmlTAG = 'monster-testclass-nan-identity-571';
593
+
594
+ if (!customElements.get(htmlTAG)) {
595
+ class NanIdentityComponent extends CustomElement {
596
+ static getTag() {
597
+ return htmlTAG;
598
+ }
599
+
600
+ get defaults() {
601
+ return Object.assign({}, super.defaults, {
602
+ measured: NaN,
603
+ unrelated: 0,
604
+ templates: {
605
+ main: '<span id="nan-measured" data-monster-replace="path:measured | call:renderMeasured"></span><span id="nan-unrelated" data-monster-replace="path:unrelated"></span>',
606
+ },
607
+ });
608
+ }
609
+
610
+ [updaterTransformerMethodsSymbol]() {
611
+ return {
612
+ renderMeasured: (value) => {
613
+ this.renderCalls = (this.renderCalls || 0) + 1;
614
+ return Number.isNaN(value) ? 'n/a' : String(value);
615
+ },
616
+ };
617
+ }
618
+ }
619
+
620
+ registerCustomElement(NanIdentityComponent);
621
+ }
622
+
623
+ const element = document.createElement(htmlTAG);
624
+ document.getElementById('test1').appendChild(element);
625
+ await new Promise((resolve) => setTimeout(resolve, 20));
626
+
627
+ expect(element.shadowRoot.querySelector('#nan-measured').textContent).to.equal('n/a');
628
+ const afterAssembly = element.renderCalls;
629
+ expect(afterAssembly).to.be.greaterThan(0);
630
+
631
+ element.setOption('unrelated', 1);
632
+ await new Promise((resolve) => setTimeout(resolve, 30));
633
+
634
+ expect(element.shadowRoot.querySelector('#nan-unrelated').textContent).to.equal('1');
635
+ expect(element.renderCalls).to.equal(afterAssembly);
636
+ });
637
+
638
+ it('should not accumulate render work for a NaN option across repeated changes issue #571', async function () {
639
+ const htmlTAG = 'monster-testclass-nan-identity-repeat-571';
640
+
641
+ if (!customElements.get(htmlTAG)) {
642
+ class NanRepeatComponent extends CustomElement {
643
+ static getTag() {
644
+ return htmlTAG;
645
+ }
646
+
647
+ get defaults() {
648
+ return Object.assign({}, super.defaults, {
649
+ measured: NaN,
650
+ counter: 0,
651
+ templates: {
652
+ main: '<span id="nan-repeat-measured" data-monster-replace="path:measured | call:renderMeasured"></span><span id="nan-repeat-counter" data-monster-replace="path:counter"></span>',
653
+ },
654
+ });
655
+ }
656
+
657
+ [updaterTransformerMethodsSymbol]() {
658
+ return {
659
+ renderMeasured: (value) => {
660
+ this.renderCalls = (this.renderCalls || 0) + 1;
661
+ return Number.isNaN(value) ? 'n/a' : String(value);
662
+ },
663
+ };
664
+ }
665
+ }
666
+
667
+ registerCustomElement(NanRepeatComponent);
668
+ }
669
+
670
+ const element = document.createElement(htmlTAG);
671
+ document.getElementById('test1').appendChild(element);
672
+ await new Promise((resolve) => setTimeout(resolve, 20));
673
+
674
+ const afterAssembly = element.renderCalls;
675
+
676
+ for (let i = 1; i <= 5; i++) {
677
+ element.setOption('counter', i);
678
+ await new Promise((resolve) => setTimeout(resolve, 20));
679
+ }
680
+
681
+ expect(element.shadowRoot.querySelector('#nan-repeat-counter').textContent).to.equal('5');
682
+ expect(element.renderCalls).to.equal(afterAssembly);
683
+ });
684
+
591
685
  it('should remove stale updater keys after replacing an option object issue #565', async function () {
592
686
  const htmlTAG = 'monster-testclass-object-sync-565';
593
687
 
@@ -986,10 +1080,270 @@ describe('DOM', function () {
986
1080
  expect(d.hasNode(t1)).to.be.true;
987
1081
 
988
1082
  })
1083
+ // net: containment that already works and must keep working
1084
+ it("hasNode should stay false for detached and foreign nodes issue #574", function () {
1085
+ let d = document.createElement('monster-testclass');
1086
+ let other = document.createElement('monster-testclass');
1087
+
1088
+ let own = document.createElement('div');
1089
+ let foreign = document.createElement('div');
1090
+ let foreignChild = document.createElement('span');
1091
+ foreign.appendChild(foreignChild);
1092
+
1093
+ d.appendChild(own);
1094
+ other.appendChild(foreign);
1095
+
1096
+ let div = document.getElementById('test1');
1097
+ div.append(d);
1098
+ div.append(other);
1099
+
1100
+ let detached = document.createElement('p');
1101
+
1102
+ expect(d.hasNode(own)).to.be.true;
1103
+ expect(d.hasNode(detached)).to.be.false;
1104
+ expect(d.hasNode(foreign)).to.be.false;
1105
+ expect(d.hasNode(foreignChild)).to.be.false;
1106
+ })
1107
+
1108
+ // regression: a node inside the shadow root of a child custom element
1109
+ it("hasNode should find a node inside the shadow root of a child element issue #574", async function () {
1110
+ const parentTAG = 'monster-testclass-hasnode-574';
1111
+ const childTAG = 'monster-testclass-hasnode-child-574';
1112
+
1113
+ if (!customElements.get(childTAG)) {
1114
+ class HasNodeChild extends CustomElement {
1115
+ static getTag() {
1116
+ return childTAG;
1117
+ }
1118
+
1119
+ get defaults() {
1120
+ return Object.assign({}, super.defaults, {
1121
+ templates: {
1122
+ main: '<span id="deep-node">deep</span>',
1123
+ },
1124
+ });
1125
+ }
1126
+ }
1127
+
1128
+ registerCustomElement(HasNodeChild);
1129
+ }
1130
+
1131
+ if (!customElements.get(parentTAG)) {
1132
+ class HasNodeParent extends CustomElement {
1133
+ static getTag() {
1134
+ return parentTAG;
1135
+ }
1136
+
1137
+ get defaults() {
1138
+ return Object.assign({}, super.defaults, {
1139
+ templates: {
1140
+ main: '<div id="host-slot"></div>',
1141
+ },
1142
+ });
1143
+ }
1144
+ }
1145
+
1146
+ registerCustomElement(HasNodeParent);
1147
+ }
1148
+
1149
+ const parent = document.createElement(parentTAG);
1150
+ document.getElementById('test1').appendChild(parent);
1151
+ await new Promise((resolve) => setTimeout(resolve, 20));
1152
+
1153
+ const child = document.createElement(childTAG);
1154
+ parent.shadowRoot.getElementById('host-slot').appendChild(child);
1155
+ await new Promise((resolve) => setTimeout(resolve, 20));
1156
+
1157
+ const deep = child.shadowRoot.getElementById('deep-node');
1158
+ expect(deep).to.not.equal(null);
1159
+
1160
+ // the child itself lives in the parent shadow root and is found today
1161
+ expect(parent.hasNode(child)).to.be.true;
1162
+ // the node inside the child shadow root is the gap being closed
1163
+ expect(parent.hasNode(deep)).to.be.true;
1164
+ })
1165
+
1166
+ it("hasNode should find a node nested two shadow roots deep issue #574", async function () {
1167
+ const outerTAG = 'monster-testclass-hasnode-outer-574';
1168
+ const middleTAG = 'monster-testclass-hasnode-middle-574';
1169
+
1170
+ if (!customElements.get(middleTAG)) {
1171
+ class HasNodeMiddle extends CustomElement {
1172
+ static getTag() {
1173
+ return middleTAG;
1174
+ }
1175
+
1176
+ get defaults() {
1177
+ return Object.assign({}, super.defaults, {
1178
+ templates: {
1179
+ main: '<div id="middle-host"></div>',
1180
+ },
1181
+ });
1182
+ }
1183
+ }
1184
+
1185
+ registerCustomElement(HasNodeMiddle);
1186
+ }
1187
+
1188
+ if (!customElements.get(outerTAG)) {
1189
+ class HasNodeOuter extends CustomElement {
1190
+ static getTag() {
1191
+ return outerTAG;
1192
+ }
1193
+
1194
+ get defaults() {
1195
+ return Object.assign({}, super.defaults, {
1196
+ templates: {
1197
+ main: '<div id="outer-host"></div>',
1198
+ },
1199
+ });
1200
+ }
1201
+ }
1202
+
1203
+ registerCustomElement(HasNodeOuter);
1204
+ }
1205
+
1206
+ const outer = document.createElement(outerTAG);
1207
+ document.getElementById('test1').appendChild(outer);
1208
+ await new Promise((resolve) => setTimeout(resolve, 20));
1209
+
1210
+ const middle = document.createElement(middleTAG);
1211
+ outer.shadowRoot.getElementById('outer-host').appendChild(middle);
1212
+ await new Promise((resolve) => setTimeout(resolve, 20));
1213
+
1214
+ const leaf = document.createElement('b');
1215
+ middle.shadowRoot.getElementById('middle-host').appendChild(leaf);
1216
+
1217
+ expect(outer.hasNode(leaf)).to.be.true;
1218
+
1219
+ const unrelated = document.createElement('b');
1220
+ expect(outer.hasNode(unrelated)).to.be.false;
1221
+ })
1222
+
989
1223
 
990
1224
 
991
1225
  })
992
1226
 
1227
+ describe('option resolution issue #576', function () {
1228
+
1229
+ let mocks;
1230
+ beforeEach(() => {
1231
+ mocks = document.getElementById('mocks');
1232
+ mocks.innerHTML = html1;
1233
+ })
1234
+
1235
+ // Pathfinder.exists() is load bearing as an input validator here:
1236
+ // the option name is undefined for every unknown
1237
+ // data-monster-option-* attribute, and the constructor plus every
1238
+ // attributeChangedCallback runs through it.
1239
+ it('should construct with an unknown data-monster-option attribute', async function () {
1240
+ const htmlTAG = 'monster-testclass-unknown-option-576';
1241
+
1242
+ if (!customElements.get(htmlTAG)) {
1243
+ class UnknownOptionComponent extends CustomElement {
1244
+ static getTag() {
1245
+ return htmlTAG;
1246
+ }
1247
+
1248
+ get defaults() {
1249
+ return Object.assign({}, super.defaults, {
1250
+ known: 'initial',
1251
+ templates: {
1252
+ main: '<span id="known-value" data-monster-replace="path:known"></span>',
1253
+ },
1254
+ });
1255
+ }
1256
+ }
1257
+
1258
+ registerCustomElement(UnknownOptionComponent);
1259
+ }
1260
+
1261
+ const element = document.createElement(htmlTAG);
1262
+ element.setAttribute('data-monster-option-foo', 'bar');
1263
+ element.setAttribute('data-monster-option-deeply-unknown-path', '1');
1264
+ document.getElementById('test1').appendChild(element);
1265
+ await new Promise((resolve) => setTimeout(resolve, 20));
1266
+
1267
+ expect(element.hasAttribute('data-monster-error')).to.be.false;
1268
+ expect(element.shadowRoot.querySelector('#known-value').textContent).to.equal('initial');
1269
+ expect(element.getOption('foo')).to.equal(undefined);
1270
+ });
1271
+
1272
+ it('should survive a runtime change of an unknown data-monster-option attribute', async function () {
1273
+ const htmlTAG = 'monster-testclass-unknown-option-runtime-576';
1274
+
1275
+ if (!customElements.get(htmlTAG)) {
1276
+ class UnknownOptionRuntimeComponent extends CustomElement {
1277
+ static getTag() {
1278
+ return htmlTAG;
1279
+ }
1280
+
1281
+ get defaults() {
1282
+ return Object.assign({}, super.defaults, {
1283
+ known: 'initial',
1284
+ templates: {
1285
+ main: '<span id="runtime-value" data-monster-replace="path:known"></span>',
1286
+ },
1287
+ });
1288
+ }
1289
+ }
1290
+
1291
+ registerCustomElement(UnknownOptionRuntimeComponent);
1292
+ }
1293
+
1294
+ const element = document.createElement(htmlTAG);
1295
+ document.getElementById('test1').appendChild(element);
1296
+ await new Promise((resolve) => setTimeout(resolve, 20));
1297
+
1298
+ element.setAttribute('data-monster-option-foo', 'bar');
1299
+ element.setAttribute('data-monster-option-known', 'changed');
1300
+ await new Promise((resolve) => setTimeout(resolve, 30));
1301
+
1302
+ expect(element.hasAttribute('data-monster-error')).to.be.false;
1303
+ expect(element.getOption('known')).to.equal('changed');
1304
+ expect(element.shadowRoot.querySelector('#runtime-value').textContent).to.equal('changed');
1305
+ });
1306
+
1307
+ it('should return declared defaults including falsy ones', async function () {
1308
+ const htmlTAG = 'monster-testclass-falsy-defaults-576';
1309
+
1310
+ if (!customElements.get(htmlTAG)) {
1311
+ class FalsyDefaultsComponent extends CustomElement {
1312
+ static getTag() {
1313
+ return htmlTAG;
1314
+ }
1315
+
1316
+ get defaults() {
1317
+ return Object.assign({}, super.defaults, {
1318
+ zero: 0,
1319
+ flag: false,
1320
+ text: '',
1321
+ nothing: null,
1322
+ absent: undefined,
1323
+ templates: {
1324
+ main: '<span></span>',
1325
+ },
1326
+ });
1327
+ }
1328
+ }
1329
+
1330
+ registerCustomElement(FalsyDefaultsComponent);
1331
+ }
1332
+
1333
+ const element = document.createElement(htmlTAG);
1334
+ document.getElementById('test1').appendChild(element);
1335
+ await new Promise((resolve) => setTimeout(resolve, 20));
1336
+
1337
+ expect(element.getOption('zero', 99)).to.equal(0);
1338
+ expect(element.getOption('flag', true)).to.equal(false);
1339
+ expect(element.getOption('text', 'fallback')).to.equal('');
1340
+ expect(element.getOption('nothing', 'fallback')).to.equal(null);
1341
+ expect(element.getOption('absent', 'fallback')).to.equal('fallback');
1342
+ expect(element.getOption('templates.main', 'fallback')).to.equal('<span></span>');
1343
+ expect(element.getOption('missing.deep.path', 'fallback')).to.equal('fallback');
1344
+ });
1345
+ });
1346
+
993
1347
  describe('mutation observer lifecycle', function () {
994
1348
  it('should coalesce child mutations without a refresh loop issue #517', async function () {
995
1349
  const htmlTAG = 'monster-testclass-mutation-loop-517';
@@ -6,6 +6,21 @@ import {getGlobal} from "../../../source/types/global.mjs";
6
6
  import {detectRuntimeEnvironment} from "../../../source/util/runtime.mjs";
7
7
 
8
8
 
9
+ /**
10
+ * Puts the real window back. Deleting the global instead would make
11
+ * initJSDOM() build a second jsdom for every file that runs later, while
12
+ * CustomElement still extends the HTMLElement of the first one, so every
13
+ * custom element defined afterwards fails to upgrade.
14
+ */
15
+ function restoreWindow(previousWindow) {
16
+ if (previousWindow === undefined) {
17
+ delete getGlobal().window;
18
+ return;
19
+ }
20
+
21
+ getGlobal().window = previousWindow;
22
+ }
23
+
9
24
  function getMockWindow(dpi) {
10
25
 
11
26
  if(detectRuntimeEnvironment() === 'browser') {
@@ -43,16 +58,19 @@ describe('dimension', () => {
43
58
  });
44
59
  })
45
60
 
61
+ let originalWindow;
62
+
46
63
  beforeEach(() => {
47
64
 
48
65
  const testDpi = 96;
49
66
  const testWindow = getMockWindow(testDpi);
67
+ originalWindow = getGlobal().window;
50
68
  getGlobal().window = testWindow;
51
69
 
52
70
  });
53
71
 
54
72
  afterEach(() => {
55
- delete getGlobal().window;
73
+ restoreWindow(originalWindow);
56
74
  });
57
75
 
58
76
  describe('convertToPixels', () => {
@@ -123,24 +141,30 @@ describe('dimension', () => {
123
141
  it('should return the correct device DPI', () => {
124
142
  const testDpi = 96;
125
143
  const testWindow = getMockWindow(testDpi);
144
+ const previousWindow = getGlobal().window;
126
145
  getGlobal().window = testWindow;
127
146
 
128
- const deviceDpi = getDeviceDPI();
129
- expect(deviceDpi).to.equal(testDpi * testWindow.devicePixelRatio);
130
-
131
- delete getGlobal().window;
147
+ try {
148
+ const deviceDpi = getDeviceDPI();
149
+ expect(deviceDpi).to.equal(testDpi * testWindow.devicePixelRatio);
150
+ } finally {
151
+ restoreWindow(previousWindow);
152
+ }
132
153
  });
133
154
 
134
155
  it('should cache the result and return the same value', () => {
135
156
  const testDpi = 96;
136
157
  const testWindow = getMockWindow(testDpi);
158
+ const previousWindow = getGlobal().window;
137
159
  getGlobal().window = testWindow;
138
160
 
139
- const deviceDpi1 = getDeviceDPI();
140
- const deviceDpi2 = getDeviceDPI();
141
- expect(deviceDpi1).to.equal(deviceDpi2);
142
-
143
- delete getGlobal().window;
161
+ try {
162
+ const deviceDpi1 = getDeviceDPI();
163
+ const deviceDpi2 = getDeviceDPI();
164
+ expect(deviceDpi1).to.equal(deviceDpi2);
165
+ } finally {
166
+ restoreWindow(previousWindow);
167
+ }
144
168
  });
145
169
  });
146
170
 
@@ -0,0 +1,198 @@
1
+ "use strict";
2
+
3
+ import * as chai from "chai";
4
+ import { getDocument } from "../../../source/dom/util.mjs";
5
+ import { chaiDom } from "../../util/chai-dom.mjs";
6
+ import { initJSDOM } from "../../util/jsdom.mjs";
7
+
8
+ let expect = chai.expect;
9
+ chai.use(chaiDom);
10
+
11
+ /**
12
+ * Regressions for issue #579.
13
+ *
14
+ * data-monster-remove had no coverage anywhere in the repository, and its
15
+ * selector relied on a :scope prefix that carries no meaning for a descendant
16
+ * query but does not resolve for shadow root content under jsdom.
17
+ */
18
+ describe("Updater remove attribute issue #579", function () {
19
+ let Updater;
20
+ let CustomElement;
21
+ let registerCustomElement;
22
+ let document;
23
+
24
+ const html = `
25
+ <div id="test1"></div>
26
+ `;
27
+
28
+ before(function (done) {
29
+ initJSDOM({})
30
+ .then(() => import("../../../source/dom/updater.mjs"))
31
+ .then((updaterModule) => {
32
+ Updater = updaterModule["Updater"];
33
+ return import("../../../source/dom/customelement.mjs");
34
+ })
35
+ .then((m) => {
36
+ try {
37
+ CustomElement = m["CustomElement"];
38
+ registerCustomElement = m["registerCustomElement"];
39
+ document = getDocument();
40
+ done();
41
+ } catch (e) {
42
+ done(e);
43
+ }
44
+ })
45
+ .catch(done);
46
+ });
47
+
48
+ beforeEach(function () {
49
+ document.getElementById("mocks").innerHTML = html;
50
+ });
51
+
52
+ afterEach(function () {
53
+ document.getElementById("mocks").innerHTML = "";
54
+ });
55
+
56
+ function settle(ms = 30) {
57
+ return new Promise((resolve) => setTimeout(resolve, ms));
58
+ }
59
+
60
+ describe("light DOM", function () {
61
+ // net: this path already worked, because :scope resolves outside a shadow
62
+ // root, and it must keep working
63
+ it("should remove annotated nodes and keep the rest", async function () {
64
+ const container = document.getElementById("test1");
65
+ container.innerHTML = `
66
+ <div id="keep-me"></div>
67
+ <div id="drop-me" data-monster-remove></div>
68
+ <span data-monster-replace="path:label"></span>
69
+ `;
70
+
71
+ const updater = new Updater(container, { label: "rendered" });
72
+ await updater.run();
73
+ await settle();
74
+
75
+ expect(container.querySelector("#drop-me")).to.equal(null);
76
+ expect(container.querySelector("#keep-me")).to.not.equal(null);
77
+ expect(container.querySelector("span").textContent).to.equal("rendered");
78
+ });
79
+
80
+ it("should remove every annotated node, nested ones included", async function () {
81
+ const container = document.getElementById("test1");
82
+ container.innerHTML = `
83
+ <div id="outer" data-monster-remove>
84
+ <div id="inner" data-monster-remove></div>
85
+ </div>
86
+ <div id="sibling"><div id="deep" data-monster-remove></div></div>
87
+ <span data-monster-replace="path:label"></span>
88
+ `;
89
+
90
+ const updater = new Updater(container, { label: "rendered" });
91
+ await updater.run();
92
+ await settle();
93
+
94
+ expect(container.querySelector("#outer")).to.equal(null);
95
+ expect(container.querySelector("#inner")).to.equal(null);
96
+ expect(container.querySelector("#deep")).to.equal(null);
97
+ expect(container.querySelector("#sibling")).to.not.equal(null);
98
+ });
99
+ });
100
+
101
+ describe("shadow DOM", function () {
102
+ const TAG = "monster-updater-remove-579";
103
+
104
+ before(function () {
105
+ if (!customElements.get(TAG)) {
106
+ class RemoveComponent extends CustomElement {
107
+ static getTag() {
108
+ return TAG;
109
+ }
110
+
111
+ get defaults() {
112
+ return Object.assign({}, super.defaults, {
113
+ label: "rendered",
114
+ templates: {
115
+ main: `
116
+ <div id="host">
117
+ <div id="keep-me"></div>
118
+ <div id="drop-me" data-monster-remove></div>
119
+ <span id="label" data-monster-replace="path:label"></span>
120
+ </div>
121
+ `,
122
+ },
123
+ });
124
+ }
125
+ }
126
+
127
+ registerCustomElement(RemoveComponent);
128
+ }
129
+ });
130
+
131
+ // regression: the annotated node survived inside a shadow root
132
+ it("should remove annotated nodes from a component shadow root", async function () {
133
+ const element = document.createElement(TAG);
134
+ document.getElementById("test1").appendChild(element);
135
+ await settle();
136
+
137
+ expect(element.shadowRoot.querySelector("#drop-me")).to.equal(null);
138
+ expect(element.shadowRoot.querySelector("#keep-me")).to.not.equal(null);
139
+ expect(element.shadowRoot.querySelector("#label").textContent).to.equal(
140
+ "rendered",
141
+ );
142
+ expect(element.hasAttribute("data-monster-error")).to.be.false;
143
+ });
144
+
145
+ it("should keep the shadow root free of annotated nodes after an update", async function () {
146
+ const element = document.createElement(TAG);
147
+ document.getElementById("test1").appendChild(element);
148
+ await settle();
149
+
150
+ element.setOption("label", "changed");
151
+ await settle();
152
+
153
+ expect(element.shadowRoot.querySelector("#label").textContent).to.equal(
154
+ "changed",
155
+ );
156
+ expect(
157
+ element.shadowRoot.querySelectorAll("[data-monster-remove]").length,
158
+ ).to.equal(0);
159
+ });
160
+
161
+ it("should remove an annotated node nested below the updater element", async function () {
162
+ const nestedTag = "monster-updater-remove-nested-579";
163
+
164
+ if (!customElements.get(nestedTag)) {
165
+ class NestedRemoveComponent extends CustomElement {
166
+ static getTag() {
167
+ return nestedTag;
168
+ }
169
+
170
+ get defaults() {
171
+ return Object.assign({}, super.defaults, {
172
+ label: "rendered",
173
+ templates: {
174
+ main: `
175
+ <div id="level-one">
176
+ <div id="level-two">
177
+ <em id="deep-drop" data-monster-remove></em>
178
+ <b id="deep-keep"></b>
179
+ </div>
180
+ </div>
181
+ `,
182
+ },
183
+ });
184
+ }
185
+ }
186
+
187
+ registerCustomElement(NestedRemoveComponent);
188
+ }
189
+
190
+ const element = document.createElement(nestedTag);
191
+ document.getElementById("test1").appendChild(element);
192
+ await settle();
193
+
194
+ expect(element.shadowRoot.querySelector("#deep-drop")).to.equal(null);
195
+ expect(element.shadowRoot.querySelector("#deep-keep")).to.not.equal(null);
196
+ });
197
+ });
198
+ });