@schukai/monster 4.153.1 → 4.154.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 (41) 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/datatable/drag-scroll.mjs +41 -9
  24. package/test/cases/components/navigation/site-navigation.mjs +10 -1
  25. package/test/cases/components/time/month-calendar.mjs +115 -0
  26. package/test/cases/data/diff.mjs +86 -0
  27. package/test/cases/data/extend.mjs +52 -0
  28. package/test/cases/data/pathfinder-resolution-baseline.mjs +322 -0
  29. package/test/cases/data/pathfinder.mjs +129 -0
  30. package/test/cases/dom/customelement-rebind-baseline.mjs +323 -0
  31. package/test/cases/dom/customelement-teardown.mjs +408 -0
  32. package/test/cases/dom/customelement.mjs +354 -0
  33. package/test/cases/dom/dimension.mjs +34 -10
  34. package/test/cases/dom/updater-remove.mjs +198 -0
  35. package/test/cases/packaging.mjs +213 -0
  36. package/test/cases/types/base.mjs +50 -0
  37. package/test/cases/types/observerlist.mjs +103 -1
  38. package/test/cases/types/proxyobserver.mjs +114 -0
  39. package/test/cases/types/regex.mjs +4 -0
  40. package/test/cases/types/tokenlist.mjs +64 -1
  41. package/test/cases/util/clone.mjs +115 -0
@@ -0,0 +1,323 @@
1
+ "use strict";
2
+
3
+ import * as chai from "chai";
4
+ import { customElementUpdaterLinkSymbol } from "../../../source/dom/constants.mjs";
5
+ import { getDocument } from "../../../source/dom/util.mjs";
6
+ import { chaiDom } from "../../util/chai-dom.mjs";
7
+ import { initJSDOM } from "../../util/jsdom.mjs";
8
+
9
+ let expect = chai.expect;
10
+ chai.use(chaiDom);
11
+
12
+ /**
13
+ * Baseline for issue #575.
14
+ *
15
+ * Before the base class starts disposing updaters on disconnect and rebinding
16
+ * on reconnect, this file pins the updater contract on a live element. Every
17
+ * assertion passes today, and the whole contract is checked twice: once on a
18
+ * freshly inserted element, and once after the element has been removed and
19
+ * appended again. The second pass is what proves a rebind actually restores
20
+ * rendering, event processing and pipe callbacks.
21
+ */
22
+ describe("CustomElement rebind baseline issue #575", function () {
23
+ let CustomElement;
24
+ let registerCustomElement;
25
+ let updaterTransformerMethodsSymbol;
26
+ let document;
27
+
28
+ const TAG = "monster-rebind-baseline-575";
29
+ const CHILD_TAG = "monster-rebind-baseline-child-575";
30
+
31
+ const html = `
32
+ <div id="test1"></div>
33
+ `;
34
+
35
+ before(function (done) {
36
+ initJSDOM({})
37
+ .then(() => import("../../../source/dom/customelement.mjs"))
38
+ .then((m) => {
39
+ try {
40
+ CustomElement = m["CustomElement"];
41
+ registerCustomElement = m["registerCustomElement"];
42
+ updaterTransformerMethodsSymbol =
43
+ m["updaterTransformerMethodsSymbol"];
44
+ document = getDocument();
45
+
46
+ if (!customElements.get(CHILD_TAG)) {
47
+ class RebindBaselineChild extends CustomElement {
48
+ static getTag() {
49
+ return CHILD_TAG;
50
+ }
51
+
52
+ get defaults() {
53
+ return Object.assign({}, super.defaults, {
54
+ label: "child-initial",
55
+ templates: {
56
+ main: '<span id="child-label" data-monster-replace="path:label"></span>',
57
+ },
58
+ });
59
+ }
60
+ }
61
+
62
+ registerCustomElement(RebindBaselineChild);
63
+ }
64
+
65
+ if (!customElements.get(TAG)) {
66
+ class RebindBaseline extends CustomElement {
67
+ static getTag() {
68
+ return TAG;
69
+ }
70
+
71
+ get defaults() {
72
+ return Object.assign({}, super.defaults, {
73
+ eventProcessing: true,
74
+ headline: "start",
75
+ shout: "quiet",
76
+ cssClass: "alpha",
77
+ inputValue: "typed",
78
+ checkedFlag: false,
79
+ patchText: "patch-start",
80
+ keyed: [
81
+ { id: "k1", label: "first" },
82
+ { id: "k2", label: "second" },
83
+ ],
84
+ items: [{ name: "item-a" }, { name: "item-b" }],
85
+ templates: {
86
+ main: `
87
+ <template id="rebind-item-575">
88
+ <li data-monster-replace="path:rebind-item-575.name"></li>
89
+ </template>
90
+ <h1 id="headline" data-monster-replace="path:headline"></h1>
91
+ <span id="shouted" data-monster-replace="path:shout | call:shout"></span>
92
+ <div id="classed" data-monster-attributes="class path:cssClass"></div>
93
+ <input id="bound" type="text" data-monster-bind="path:inputValue" data-monster-attributes="value path:inputValue">
94
+ <input id="flag" type="checkbox" data-monster-properties="checked path:checkedFlag">
95
+ <div id="patched" data-monster-patch="path:patchText"></div>
96
+ <div id="keyed" data-monster-patch="path:keyed" data-monster-patch-key="path:id" data-monster-patch-render="path:label"></div>
97
+ <ul id="inserted" data-monster-insert="rebind-item-575 path:items"></ul>
98
+ <select id="native-select"><option>one</option></select>
99
+ <${CHILD_TAG} id="child"></${CHILD_TAG}>
100
+ `,
101
+ },
102
+ });
103
+ }
104
+
105
+ [updaterTransformerMethodsSymbol]() {
106
+ return {
107
+ shout: (value) => String(value).toUpperCase(),
108
+ };
109
+ }
110
+ }
111
+
112
+ registerCustomElement(RebindBaseline);
113
+ }
114
+
115
+ done();
116
+ } catch (e) {
117
+ done(e);
118
+ }
119
+ })
120
+ .catch(done);
121
+ });
122
+
123
+ let element;
124
+
125
+ beforeEach(async function () {
126
+ const mocks = document.getElementById("mocks");
127
+ mocks.innerHTML = html;
128
+
129
+ element = document.createElement(TAG);
130
+ document.getElementById("test1").appendChild(element);
131
+ await settle();
132
+ });
133
+
134
+ afterEach(function () {
135
+ const mocks = document.getElementById("mocks");
136
+ mocks.innerHTML = "";
137
+ });
138
+
139
+ function settle(ms = 30) {
140
+ return new Promise((resolve) => setTimeout(resolve, ms));
141
+ }
142
+
143
+ function query(id) {
144
+ return element.shadowRoot.querySelector(`#${id}`);
145
+ }
146
+
147
+ async function reconnect() {
148
+ const parent = element.parentNode;
149
+ element.remove();
150
+ await settle();
151
+ parent.appendChild(element);
152
+ await settle();
153
+ }
154
+
155
+ /**
156
+ * The whole updater contract. Called once on a fresh element and once after a
157
+ * reconnect, so both runs assert exactly the same behaviour.
158
+ */
159
+ async function assertUpdaterContract(label) {
160
+ const because = ` (${label})`;
161
+
162
+ expect(element.hasAttribute("data-monster-error"), `no error${because}`).to
163
+ .be.false;
164
+
165
+ // data-monster-replace
166
+ element.setOption("headline", `headline-${label}`);
167
+ await settle();
168
+ expect(query("headline").textContent, `replace${because}`).to.equal(
169
+ `headline-${label}`,
170
+ );
171
+
172
+ // pipe callback from updaterTransformerMethodsSymbol
173
+ element.setOption("shout", `shout-${label}`);
174
+ await settle();
175
+ expect(query("shouted").textContent, `pipe${because}`).to.equal(
176
+ `shout-${label}`.toUpperCase(),
177
+ );
178
+
179
+ // data-monster-attributes
180
+ element.setOption("cssClass", `class-${label}`);
181
+ await settle();
182
+ expect(query("classed").getAttribute("class"), `attributes${because}`).to.
183
+ equal(`class-${label}`);
184
+
185
+ // data-monster-properties
186
+ element.setOption("checkedFlag", true);
187
+ await settle();
188
+ expect(query("flag").checked, `properties on${because}`).to.equal(true);
189
+ element.setOption("checkedFlag", false);
190
+ await settle();
191
+ expect(query("flag").checked, `properties off${because}`).to.equal(false);
192
+
193
+ // data-monster-patch
194
+ element.setOption("patchText", `patch-${label}`);
195
+ await settle();
196
+ expect(query("patched").textContent, `patch${because}`).to.equal(
197
+ `patch-${label}`,
198
+ );
199
+
200
+ // keyed patch renders one node per key and keeps node identity for a key
201
+ // whose data did not change
202
+ const keyedNodes = [...query("keyed").childNodes];
203
+ expect(keyedNodes.length, `keyed count${because}`).to.equal(2);
204
+ const firstKeyedNode = keyedNodes[0];
205
+ const firstLabel = element.getOption("keyed.0.label");
206
+
207
+ element.setOption("keyed.1.label", `second-${label}`);
208
+ await settle();
209
+
210
+ expect(
211
+ [...query("keyed").childNodes][0],
212
+ `keyed identity${because}`,
213
+ ).to.equal(firstKeyedNode);
214
+ expect(
215
+ [...query("keyed").childNodes].map((node) => node.textContent),
216
+ `keyed render${because}`,
217
+ ).to.deep.equal([firstLabel, `second-${label}`]);
218
+
219
+ // data-monster-insert
220
+ expect(query("inserted").children.length, `insert${because}`).to.equal(2);
221
+ element.setOption("items.0.name", `item-${label}`);
222
+ await settle();
223
+ expect(query("inserted").textContent, `insert update${because}`).to.contain(
224
+ `item-${label}`,
225
+ );
226
+
227
+ // nested component renders its own options
228
+ const child = query("child");
229
+ expect(child, `child present${because}`).to.not.equal(null);
230
+ child.setOption("label", `child-${label}`);
231
+ await settle();
232
+ expect(
233
+ child.shadowRoot.querySelector("#child-label").textContent,
234
+ `child update${because}`,
235
+ ).to.equal(`child-${label}`);
236
+
237
+ // event processing writes a typed value into the updater subject, which is
238
+ // the options snapshot, not the component options. The one way sync means
239
+ // getOption() never sees user input, so the snapshot is the only place the
240
+ // value lives and a rebind must not discard it.
241
+ const bound = query("bound");
242
+ bound.value = `typed-${label}`;
243
+ bound.dispatchEvent(new Event("input", { bubbles: true, composed: true }));
244
+ await settle(150);
245
+ expect(
246
+ element.getInternalUpdateCloneData()?.inputValue,
247
+ `bind writeback${because}`,
248
+ ).to.equal(`typed-${label}`);
249
+
250
+ // disabled propagation reaches native controls inside the shadow root
251
+ element.setOption("disabled", true);
252
+ await settle();
253
+ expect(
254
+ query("native-select").hasAttribute("disabled"),
255
+ `disabled${because}`,
256
+ ).to.be.true;
257
+ element.setOption("disabled", false);
258
+ await settle();
259
+ expect(
260
+ query("native-select").hasAttribute("disabled"),
261
+ `disabled off${because}`,
262
+ ).to.be.false;
263
+ }
264
+
265
+ it("should honor the whole updater contract on a fresh element", async function () {
266
+ await assertUpdaterContract("fresh");
267
+ });
268
+
269
+ it("should honor the whole updater contract again after a reconnect", async function () {
270
+ await assertUpdaterContract("before-reconnect");
271
+ await reconnect();
272
+ await assertUpdaterContract("after-reconnect");
273
+ });
274
+
275
+ it("should keep the shadow root and its nodes across a reconnect", async function () {
276
+ const shadowRoot = element.shadowRoot;
277
+ const headline = query("headline");
278
+ const childCount = shadowRoot.childNodes.length;
279
+
280
+ await reconnect();
281
+
282
+ expect(element.shadowRoot).to.equal(shadowRoot);
283
+ expect(query("headline")).to.equal(headline);
284
+ expect(element.shadowRoot.childNodes.length).to.equal(childCount);
285
+ expect(element.children.length).to.equal(0);
286
+ expect(element.hasAttribute("data-monster-error")).to.be.false;
287
+ });
288
+
289
+ it("should keep option object identity across a reconnect", async function () {
290
+ const items = element.getOption("items");
291
+ const keyed = element.getOption("keyed");
292
+
293
+ element.setOption("headline", "before");
294
+ await settle();
295
+
296
+ await reconnect();
297
+
298
+ expect(element.getOption("items")).to.equal(items);
299
+ expect(element.getOption("keyed")).to.equal(keyed);
300
+ expect(element.getOption("headline")).to.equal("before");
301
+ });
302
+
303
+ it("should keep exactly one updater group linked across a reconnect", async function () {
304
+ function countUpdaters() {
305
+ let count = 0;
306
+ for (const group of element[customElementUpdaterLinkSymbol] ?? []) {
307
+ for (const updater of group) {
308
+ if (typeof updater?.isDisposed === "function") {
309
+ count++;
310
+ }
311
+ }
312
+ }
313
+ return count;
314
+ }
315
+
316
+ const before = countUpdaters();
317
+ expect(before).to.be.greaterThan(0);
318
+
319
+ await reconnect();
320
+
321
+ expect(countUpdaters()).to.equal(before);
322
+ });
323
+ });