@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.
- package/package.json +1 -1
- package/source/components/form/message-state-button.mjs +1 -1
- package/source/components/form/select-link.mjs +12 -1
- package/source/components/state/log.mjs +3 -7
- package/source/components/state/thread.mjs +3 -7
- package/source/components/time/day.mjs +0 -19
- package/source/components/time/month-calendar.mjs +5 -1
- package/source/data/buildmap.mjs +3 -3
- package/source/data/datasource.mjs +3 -10
- package/source/data/diff.mjs +13 -12
- package/source/data/extend.mjs +23 -1
- package/source/data/pathfinder.mjs +186 -19
- package/source/dom/customelement.mjs +273 -30
- package/source/dom/updater.mjs +6 -2
- package/source/types/base.mjs +7 -6
- package/source/types/basewithoptions.mjs +1 -8
- package/source/types/internal.mjs +4 -10
- package/source/types/observerlist.mjs +9 -17
- package/source/types/proxyobserver.mjs +19 -2
- package/source/types/tokenlist.mjs +13 -8
- package/source/util/clone.mjs +44 -14
- package/test/cases/browser-bundle.mjs +75 -0
- package/test/cases/components/datatable/drag-scroll.mjs +41 -9
- package/test/cases/components/navigation/site-navigation.mjs +10 -1
- package/test/cases/components/time/month-calendar.mjs +115 -0
- package/test/cases/data/diff.mjs +86 -0
- package/test/cases/data/extend.mjs +52 -0
- package/test/cases/data/pathfinder-resolution-baseline.mjs +322 -0
- package/test/cases/data/pathfinder.mjs +129 -0
- package/test/cases/dom/customelement-rebind-baseline.mjs +323 -0
- package/test/cases/dom/customelement-teardown.mjs +408 -0
- package/test/cases/dom/customelement.mjs +354 -0
- package/test/cases/dom/dimension.mjs +34 -10
- package/test/cases/dom/updater-remove.mjs +198 -0
- package/test/cases/packaging.mjs +213 -0
- package/test/cases/types/base.mjs +50 -0
- package/test/cases/types/observerlist.mjs +103 -1
- package/test/cases/types/proxyobserver.mjs +114 -0
- package/test/cases/types/regex.mjs +4 -0
- package/test/cases/types/tokenlist.mjs +64 -1
- 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
|
+
});
|