@schukai/monster 4.153.0 → 4.153.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 CHANGED
@@ -1 +1 @@
1
- {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","files":["source","test/cases","test/util","CHANGELOG.md"],"homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.153.0"}
1
+ {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","files":["source","test/cases","test/util","CHANGELOG.md"],"homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.153.1"}
@@ -362,7 +362,8 @@ function buildFlatMap(subject, selector, key, parentMap) {
362
362
  * @return {*}
363
363
  */
364
364
  export function build(subject, definition, defaultValue) {
365
- if (definition === undefined) return defaultValue ? defaultValue : subject;
365
+ if (definition === undefined)
366
+ return defaultValue !== undefined ? defaultValue : subject;
366
367
  validateString(definition);
367
368
 
368
369
  const regexp = /(?<placeholder>\${(?<path>[a-z\^A-Z.\-_0-9]*)})/gm;
@@ -384,7 +385,7 @@ export function build(subject, definition, defaultValue) {
384
385
  let v = finder.getVia(path);
385
386
  if (v === undefined) v = defaultValue;
386
387
 
387
- definition = definition.replaceAll(placeholder, v);
388
+ definition = definition.replaceAll(placeholder, () => `${v}`);
388
389
  });
389
390
 
390
391
  return definition;
@@ -21,12 +21,6 @@ import { extend } from "./extend.mjs";
21
21
 
22
22
  export { buildTree };
23
23
 
24
- /**
25
- * @private
26
- * @type {symbol}
27
- */
28
- const parentSymbol = Symbol("parent");
29
-
30
24
  /**
31
25
  * @private
32
26
  * @type {symbol}
@@ -171,8 +165,6 @@ function buildTree(subject, selector, idKey, parentIDKey, options) {
171
165
  throw new Error("the object has no value for the specified id");
172
166
  }
173
167
 
174
- o[parentSymbol] = ref;
175
-
176
168
  const node = new Node(o);
177
169
  this.has(ref)
178
170
  ? this.get(ref).add(node)
@@ -217,16 +217,24 @@ function iterate(subject, path, check) {
217
217
 
218
218
  const result = new Map();
219
219
 
220
- if (isArray(path)) {
221
- path = path.join(DELIMITER);
222
- }
223
-
224
- if (isObject(subject) || isArray(subject)) {
220
+ if (subject instanceof Map) {
221
+ for (const [key, value] of subject) {
222
+ result.set(key, getValueViaPath.call(this, value, path, check));
223
+ }
224
+ } else if (subject instanceof Set) {
225
+ let index = 0;
226
+ for (const value of subject) {
227
+ result.set(
228
+ `${index++}`,
229
+ getValueViaPath.call(this, value, path, check),
230
+ );
231
+ }
232
+ } else if (isObject(subject) || isArray(subject)) {
225
233
  for (const [key, value] of Object.entries(subject)) {
226
234
  result.set(key, getValueViaPath.call(this, value, path, check));
227
235
  }
228
236
  } else {
229
- const key = path.split(DELIMITER).shift();
237
+ const key = isArray(path) ? path[0] : path.split(DELIMITER).shift();
230
238
  result.set(key, getValueViaPath.call(this, subject, path, check));
231
239
  }
232
240
 
@@ -272,7 +280,7 @@ function getValueViaPath(subject, path, check) {
272
280
  let current = parts.shift();
273
281
 
274
282
  if (current === this.wildCard) {
275
- return iterate.call(this, subject, parts.join(DELIMITER), check);
283
+ return iterate.call(this, subject, parts, check);
276
284
  }
277
285
 
278
286
  if (isObject(subject) || isArray(subject)) {
@@ -280,23 +288,21 @@ function getValueViaPath(subject, path, check) {
280
288
  if (subject instanceof Map || subject instanceof WeakMap) {
281
289
  anchor = subject.get(current);
282
290
  } else if (subject instanceof Set) {
283
- current = parseInt(current);
284
- validateInteger(current);
291
+ current = parsePathIndex(current);
285
292
  anchor = [...subject]?.[current];
286
293
  } else if (subject instanceof WeakSet) {
287
294
  throw Error("unsupported action for this data type (WeakSet)");
288
295
  } else if (typeof WeakRef === "function" && subject instanceof WeakRef) {
289
296
  throw Error("unsupported action for this data type (WeakRef)");
290
297
  } else if (isArray(subject)) {
291
- current = parseInt(current);
292
- validateInteger(current);
298
+ current = parsePathIndex(current);
293
299
  anchor = subject?.[current];
294
300
  } else {
295
301
  anchor = subject?.[current];
296
302
  }
297
303
 
298
304
  if (isObject(anchor) || isArray(anchor)) {
299
- return getValueViaPath.call(this, anchor, parts.join(DELIMITER), check);
305
+ return getValueViaPath.call(this, anchor, parts, check);
300
306
  }
301
307
 
302
308
  if (parts.length > 0) {
@@ -304,12 +310,18 @@ function getValueViaPath(subject, path, check) {
304
310
  }
305
311
 
306
312
  if (check === true) {
307
- const descriptor = Object.getOwnPropertyDescriptor(
308
- Object.getPrototypeOf(subject),
309
- current,
310
- );
313
+ let exists;
314
+ if (subject instanceof Map || subject instanceof WeakMap) {
315
+ exists = subject.has(current);
316
+ } else if (subject instanceof Set) {
317
+ exists = current >= 0 && current < subject.size;
318
+ } else if (isArray(subject)) {
319
+ exists = Reflect.has(subject, current);
320
+ } else {
321
+ exists = Reflect.has(subject, current);
322
+ }
311
323
 
312
- if (!subject.hasOwnProperty(current) && descriptor === undefined) {
324
+ if (!exists) {
313
325
  throw Error("unknown value " + current);
314
326
  }
315
327
  }
@@ -411,7 +423,7 @@ function setValueViaPath(subject, path, value) {
411
423
  * @param {*} value
412
424
  */
413
425
  function assignProperty(object, key, value) {
414
- if (!object.hasOwnProperty(key)) {
426
+ if (!Object.hasOwn(object, key)) {
415
427
  object[key] = value;
416
428
  return;
417
429
  }
@@ -423,6 +435,21 @@ function assignProperty(object, key, value) {
423
435
  object[key] = value;
424
436
  }
425
437
 
438
+ /**
439
+ * Convert a complete integer path segment without accepting trailing text.
440
+ * @param {*} value
441
+ * @return {number}
442
+ * @private
443
+ */
444
+ function parsePathIndex(value) {
445
+ if (!/^-?\d+$/.test(`${value}`)) {
446
+ throw new TypeError("value is not an integer");
447
+ }
448
+ const index = Number(value);
449
+ validateInteger(index);
450
+ return index;
451
+ }
452
+
426
453
  /**
427
454
  *
428
455
  * @param {object} subject
@@ -486,7 +513,9 @@ function deleteValueViaPath(subject, path) {
486
513
  * @private
487
514
  */
488
515
  function validateMutationPath(path) {
489
- const parts = isArray(path) ? path : path.split(DELIMITER);
516
+ const parts = (isArray(path) ? path : [path]).flatMap((part) =>
517
+ `${part}`.split(DELIMITER),
518
+ );
490
519
  for (const part of parts) {
491
520
  if (UNSAFE_MUTATION_PATH_SEGMENTS.has(`${part}`)) {
492
521
  throw new Error(`unsafe mutation path segment: ${part}`);
@@ -530,7 +530,7 @@ function transform(value) {
530
530
 
531
531
  const useKey = keys?.[key];
532
532
 
533
- if (value?.[useKey]) {
533
+ if (useKey !== undefined && Reflect.has(value, useKey)) {
534
534
  return value?.[useKey];
535
535
  }
536
536
 
@@ -555,7 +555,7 @@ function transform(value) {
555
555
  }
556
556
 
557
557
  if (isObject(value) || isArray(value)) {
558
- if (value?.[key]) {
558
+ if (Reflect.has(value, key)) {
559
559
  return value?.[key];
560
560
  }
561
561
 
@@ -377,6 +377,7 @@ function getInternal() {
377
377
  }
378
378
 
379
379
  const debounceValueSymbol = Symbol("debounceValue");
380
+ const pendingValueAttributeSymbol = Symbol("pendingValueAttribute");
380
381
 
381
382
  /**
382
383
  *
@@ -388,7 +389,17 @@ const debounceValueSymbol = Symbol("debounceValue");
388
389
  */
389
390
  function initValueAttributeObserver() {
390
391
  const self = this;
391
- this[attributeObserverSymbol]["value"] = () => {
392
+ this[attributeObserverSymbol]["value"] = (newValue, oldValue) => {
393
+ if (!self[pendingValueAttributeSymbol]) {
394
+ self[pendingValueAttributeSymbol] = {
395
+ oldValue: normalizeAttributeValue(oldValue),
396
+ newValue: normalizeAttributeValue(newValue),
397
+ };
398
+ } else {
399
+ self[pendingValueAttributeSymbol].newValue =
400
+ normalizeAttributeValue(newValue);
401
+ }
402
+
392
403
  if (self[debounceValueSymbol] instanceof DeadMansSwitch) {
393
404
  try {
394
405
  self[debounceValueSymbol].touch();
@@ -402,18 +413,33 @@ function initValueAttributeObserver() {
402
413
  }
403
414
 
404
415
  self[debounceValueSymbol] = new DeadMansSwitch(10, () => {
405
- const oldValue = self.getAttribute("value");
406
- const newValue = self.value;
407
-
408
- if (oldValue !== newValue) {
409
- setTimeout(() => {
410
- self.value = self.getAttribute("value") ?? "";
411
- }, 0);
412
- }
416
+ const pending = self[pendingValueAttributeSymbol];
417
+ delete self[pendingValueAttributeSymbol];
418
+ delete self[debounceValueSymbol];
419
+ if (!pending) return;
420
+
421
+ setTimeout(() => {
422
+ const currentValue = normalizeAttributeValue(self.value);
423
+ if (
424
+ currentValue === pending.oldValue &&
425
+ currentValue !== pending.newValue
426
+ ) {
427
+ self.value = pending.newValue;
428
+ }
429
+ }, 0);
413
430
  });
414
431
  };
415
432
  }
416
433
 
434
+ /**
435
+ * @param {*} value
436
+ * @return {string}
437
+ * @private
438
+ */
439
+ function normalizeAttributeValue(value) {
440
+ return value === null || value === undefined ? "" : String(value);
441
+ }
442
+
417
443
  const disabledSyncSymbol = Symbol("disabledSync");
418
444
  const disabledAttributeSyncSymbol = Symbol("disabledAttributeSync");
419
445
 
@@ -734,7 +734,10 @@ class CustomElement extends HTMLElement {
734
734
  // Initialize the custom elements inside the shadow root and the slotted elements
735
735
  initFromCallbackHost.call(this);
736
736
  try {
737
- nodeList = new Set([...elements, ...getSlottedElements.call(this)]);
737
+ nodeList = new Set([
738
+ ...elements,
739
+ ...getSlottedElements.call(this, undefined, undefined, getManagedShadowRoot.call(this)),
740
+ ]);
738
741
  } catch (e) {
739
742
  nodeList = elements;
740
743
  }
@@ -1133,7 +1136,12 @@ function initOptionObserver() {
1133
1136
  targets.add(element);
1134
1137
  }
1135
1138
  }
1136
- for (const element of getSlottedElements.call(self, disabledQuery)) {
1139
+ for (const element of getSlottedElements.call(
1140
+ self,
1141
+ disabledQuery,
1142
+ undefined,
1143
+ getManagedShadowRoot.call(self),
1144
+ )) {
1137
1145
  targets.add(element);
1138
1146
  }
1139
1147
  } catch (e) {}
@@ -1293,6 +1301,17 @@ function syncUpdaterSubject(target, source) {
1293
1301
  return;
1294
1302
  }
1295
1303
 
1304
+ // Empty option objects are also used as refresh markers by editable
1305
+ // datasource components. Keep their established merge behavior while
1306
+ // allowing non-empty replacement objects to remove stale keys.
1307
+ if (Object.keys(source).length > 0) {
1308
+ for (const key of Object.keys(target)) {
1309
+ if (!Object.hasOwn(source, key)) {
1310
+ delete target[key];
1311
+ }
1312
+ }
1313
+ }
1314
+
1296
1315
  for (const [key, value] of Object.entries(source)) {
1297
1316
  syncUpdaterValue(target, key, value);
1298
1317
  }
@@ -27,37 +27,33 @@ export { getSlottedElements, getSlottedNodes };
27
27
  * @since 3.33.0
28
28
  * @throws {Error} query must be a string
29
29
  */
30
- function getSlottedNodes(query, name) {
30
+ function getSlottedNodes(query, name, managedShadowRoot) {
31
31
  const result = new Set();
32
+ const shadowRoot = getShadowRoot(this, managedShadowRoot);
32
33
 
33
- if (!this.shadowRoot) {
34
+ if (!(shadowRoot instanceof ShadowRoot)) {
34
35
  return result;
35
36
  }
36
37
 
37
- let selector = "slot";
38
- if (name !== undefined) {
39
- if (name === null) {
40
- selector += ":not([name])";
41
- } else {
42
- selector += `[name=${validateString(name)}]`;
43
- }
44
- }
45
-
46
- const slots = this.shadowRoot.querySelectorAll(selector);
38
+ const slots = getSlotsByName(shadowRoot, name);
47
39
 
48
- for (const [, slot] of Object.entries(slots)) {
40
+ for (const slot of slots) {
49
41
  slot.assignedNodes().forEach(function (node) {
50
42
  if (node === null || node === undefined) {
51
43
  return;
52
44
  }
53
45
 
54
46
  if (isString(query)) {
55
- node.querySelectorAll(query).forEach(function (n) {
56
- result.add(n);
57
- });
58
-
59
- if (node.matches(query)) {
47
+ if (query.length === 0) {
60
48
  result.add(node);
49
+ } else if (node instanceof Element) {
50
+ node.querySelectorAll(query).forEach(function (n) {
51
+ result.add(n);
52
+ });
53
+
54
+ if (node.matches(query)) {
55
+ result.add(node);
56
+ }
61
57
  }
62
58
  } else if (query !== undefined) {
63
59
  throw new Error("query must be a string");
@@ -80,25 +76,17 @@ function getSlottedNodes(query, name) {
80
76
  * @since 1.23.0
81
77
  * @throws {Error} query must be a string
82
78
  */
83
- function getSlottedElements(query, name) {
79
+ function getSlottedElements(query, name, managedShadowRoot) {
84
80
  const result = new Set();
81
+ const shadowRoot = getShadowRoot(this, managedShadowRoot);
85
82
 
86
- if (!(this.shadowRoot instanceof ShadowRoot)) {
83
+ if (!(shadowRoot instanceof ShadowRoot)) {
87
84
  return result;
88
85
  }
89
86
 
90
- let selector = "slot";
91
- if (name !== undefined) {
92
- if (name === null) {
93
- selector += ":not([name])";
94
- } else {
95
- selector += `[name=${validateString(name)}]`;
96
- }
97
- }
98
-
99
- const slots = this.shadowRoot.querySelectorAll(selector);
87
+ const slots = getSlotsByName(shadowRoot, name);
100
88
 
101
- for (const [, slot] of Object.entries(slots)) {
89
+ for (const slot of slots) {
102
90
  slot.assignedElements().forEach(function (node) {
103
91
  if (
104
92
  !(node instanceof HTMLElement) &&
@@ -129,3 +117,30 @@ function getSlottedElements(query, name) {
129
117
 
130
118
  return result;
131
119
  }
120
+
121
+ /**
122
+ * @param {*} host
123
+ * @param {*} managedShadowRoot
124
+ * @return {ShadowRoot|null}
125
+ * @private
126
+ */
127
+ function getShadowRoot(host, managedShadowRoot) {
128
+ if (managedShadowRoot instanceof ShadowRoot) return managedShadowRoot;
129
+ return host?.shadowRoot instanceof ShadowRoot ? host.shadowRoot : null;
130
+ }
131
+
132
+ /**
133
+ * Filter slots by their DOM name instead of interpolating an unsafe selector.
134
+ * @param {ShadowRoot} shadowRoot
135
+ * @param {string|undefined|null} name
136
+ * @return {HTMLSlotElement[]}
137
+ * @private
138
+ */
139
+ function getSlotsByName(shadowRoot, name) {
140
+ if (name !== undefined && name !== null) validateString(name);
141
+ return Array.from(shadowRoot.querySelectorAll("slot")).filter((slot) => {
142
+ if (name === undefined) return true;
143
+ if (name === null) return slot.name === "";
144
+ return slot.name === name;
145
+ });
146
+ }
@@ -156,7 +156,10 @@ export function findDocumentTemplate(id, currentNode) {
156
156
  )
157
157
  ) {
158
158
  if (currentNode instanceof Node) {
159
- if (currentNode.hasAttribute(ATTRIBUTE_TEMPLATE_PREFIX)) {
159
+ if (
160
+ currentNode instanceof Element &&
161
+ currentNode.hasAttribute(ATTRIBUTE_TEMPLATE_PREFIX)
162
+ ) {
160
163
  prefixID = currentNode.getAttribute(ATTRIBUTE_TEMPLATE_PREFIX);
161
164
  }
162
165
 
@@ -803,17 +803,34 @@ function parseIntArray(val) {
803
803
  * @private
804
804
  */
805
805
  function retrieveFromBindings() {
806
- if (this[internalSymbol].element.matches(`[${ATTRIBUTE_UPDATER_BIND}]`)) {
806
+ if (
807
+ this[internalSymbol].element.matches(`[${ATTRIBUTE_UPDATER_BIND}]`) &&
808
+ !isUncheckedRadio(this[internalSymbol].element)
809
+ ) {
807
810
  retrieveAndSetValue.call(this, this[internalSymbol].element);
808
811
  }
809
812
 
810
813
  for (const [, element] of this[internalSymbol].element
811
814
  .querySelectorAll(`[${ATTRIBUTE_UPDATER_BIND}]`)
812
815
  .entries()) {
816
+ if (isUncheckedRadio(element)) continue;
813
817
  retrieveAndSetValue.call(this, element);
814
818
  }
815
819
  }
816
820
 
821
+ /**
822
+ * @param {HTMLElement} element
823
+ * @return {boolean}
824
+ * @private
825
+ */
826
+ function isUncheckedRadio(element) {
827
+ return (
828
+ element instanceof HTMLInputElement &&
829
+ element.type === "radio" &&
830
+ element.checked === false
831
+ );
832
+ }
833
+
817
834
  /**
818
835
  * @private
819
836
  * @license AGPLv3
@@ -1201,7 +1218,10 @@ function getSlots(container, cache) {
1201
1218
  return cache.get(container);
1202
1219
  }
1203
1220
 
1204
- const slots = container.querySelectorAll("slot");
1221
+ const slots = Array.from(container.querySelectorAll("slot"));
1222
+ if (container.localName === "slot") {
1223
+ slots.unshift(container);
1224
+ }
1205
1225
  cache.set(container, slots);
1206
1226
  return slots;
1207
1227
  }
@@ -1565,6 +1585,9 @@ function replaceKeyedPatchedChildren(
1565
1585
  const nextNode = preparePatchedNode(reused, renderedValue);
1566
1586
  nextNode[patchNodeKeySymbol] = normalizedKey;
1567
1587
  if (nextNode !== reused && reused.parentNode === element) {
1588
+ if (reused instanceof HTMLElement) {
1589
+ teardownManagedSubtree(reused);
1590
+ }
1568
1591
  element.replaceChild(nextNode, reused);
1569
1592
  }
1570
1593
  nextNodes.push(nextNode);
@@ -1591,15 +1614,11 @@ function replaceKeyedPatchedChildren(
1591
1614
  }
1592
1615
  }
1593
1616
 
1594
- for (const node of nextNodes) {
1595
- if (node.parentNode !== element) {
1596
- element.appendChild(node);
1597
- continue;
1598
- }
1599
-
1600
- if (element.lastChild !== node) {
1601
- element.appendChild(node);
1602
- }
1617
+ for (let index = 0; index < nextNodes.length; index++) {
1618
+ const node = nextNodes[index];
1619
+ const current = element.childNodes[index] ?? null;
1620
+ if (current === node) continue;
1621
+ element.insertBefore(node, current);
1603
1622
  }
1604
1623
  }
1605
1624
 
@@ -1851,8 +1870,7 @@ function runUpdateAttributes(
1851
1870
  if (
1852
1871
  !element.hasAttribute(ATTRIBUTE_UPDATER_SELECT_THIS) &&
1853
1872
  !attributes.includes(`path:${current}`) &&
1854
- !attributes.startsWith("static:") &&
1855
- !attributes.startsWith("i18n:")
1873
+ !hasUnconditionalBinding(attributes)
1856
1874
  ) {
1857
1875
  continue;
1858
1876
  }
@@ -1933,8 +1951,7 @@ function runUpdateProperties(
1933
1951
  if (
1934
1952
  !element.hasAttribute(ATTRIBUTE_UPDATER_SELECT_THIS) &&
1935
1953
  !properties.includes(`path:${current}`) &&
1936
- !properties.startsWith("static:") &&
1937
- !properties.startsWith("i18n:")
1954
+ !hasUnconditionalBinding(properties)
1938
1955
  ) {
1939
1956
  continue;
1940
1957
  }
@@ -1981,6 +1998,9 @@ function runUpdateProperties(
1981
1998
 
1982
1999
  function handleInputControlAttributeUpdate(element, name, value) {
1983
2000
  if (element instanceof HTMLSelectElement) {
2001
+ if (name !== "value") {
2002
+ return;
2003
+ }
1984
2004
  switch (element.type) {
1985
2005
  case "select-multiple":
1986
2006
  const selectedValues = isArray(value)
@@ -2054,6 +2074,22 @@ function handleInputControlAttributeUpdate(element, name, value) {
2054
2074
  }
2055
2075
  }
2056
2076
 
2077
+ /**
2078
+ * Check whether an attribute/property definition contains a command that is
2079
+ * independent from the changed subject path.
2080
+ * @param {string} definitions
2081
+ * @return {boolean}
2082
+ * @private
2083
+ */
2084
+ function hasUnconditionalBinding(definitions) {
2085
+ return definitions.split(",").some((definition) => {
2086
+ const separator = definition.indexOf(" ");
2087
+ if (separator < 0) return false;
2088
+ const command = trimSpaces(definition.substring(separator + 1));
2089
+ return command.startsWith("static:") || command.startsWith("i18n:");
2090
+ });
2091
+ }
2092
+
2057
2093
  /**
2058
2094
  * @private
2059
2095
  * @param {HTMLElement|*} element
@@ -199,8 +199,6 @@ class Node extends Base {
199
199
  * @return {setChildLevelAndParent}
200
200
  */
201
201
  function setChildLevelAndParent(node, operand, visitedNodes) {
202
- const self = this;
203
-
204
202
  if (visitedNodes.has(node)) {
205
203
  throw new Error(
206
204
  "the node has already been visited and cannot be traversed again",
@@ -217,7 +215,7 @@ function setChildLevelAndParent(node, operand, visitedNodes) {
217
215
  child[treeStructureSymbol].parent = node;
218
216
  child[treeStructureSymbol].level =
219
217
  node[treeStructureSymbol].level + operand;
220
- setChildLevelAndParent.call(self, child, operand, visitedNodes);
218
+ setChildLevelAndParent.call(node, child, operand, visitedNodes);
221
219
  });
222
220
  return this;
223
221
  }
@@ -156,6 +156,15 @@ function getHandler() {
156
156
  }
157
157
 
158
158
  const value = Reflect.get(target, key, receiver);
159
+ const descriptor = Reflect.getOwnPropertyDescriptor(target, key);
160
+ if (
161
+ descriptor &&
162
+ descriptor.configurable === false &&
163
+ "value" in descriptor &&
164
+ descriptor.writable === false
165
+ ) {
166
+ return value;
167
+ }
159
168
 
160
169
  if (typeof key === "symbol") {
161
170
  return value;
@@ -233,16 +242,12 @@ function getHandler() {
233
242
 
234
243
  // https://262.ecma-international.org/9.0/#sec-proxy-object-internal-methods-and-internal-slots-delete-p
235
244
  deleteProperty: function (target, key) {
236
- if (key in target) {
237
- delete target[key];
238
-
239
- if (typeof key !== "symbol") {
240
- proxy.observers.notify(proxy);
241
- }
242
-
243
- return true;
245
+ const hadOwnProperty = Object.hasOwn(target, key);
246
+ const result = Reflect.deleteProperty(target, key);
247
+ if (result && hadOwnProperty && typeof key !== "symbol") {
248
+ proxy.observers.notify(proxy);
244
249
  }
245
- return false;
250
+ return result;
246
251
  },
247
252
 
248
253
  // https://262.ecma-international.org/9.0/#sec-proxy-object-internal-methods-and-internal-slots-defineownproperty-p-desc
@@ -277,5 +282,22 @@ function getHandler() {
277
282
  * @private
278
283
  */
279
284
  function isNativeObjectWithInternalSlots(value) {
280
- return value instanceof Date || value instanceof Map || value instanceof Set;
285
+ if (
286
+ value instanceof Date ||
287
+ value instanceof RegExp ||
288
+ value instanceof Map ||
289
+ value instanceof Set ||
290
+ value instanceof WeakMap ||
291
+ value instanceof WeakSet ||
292
+ value instanceof Promise ||
293
+ value instanceof ArrayBuffer ||
294
+ ArrayBuffer.isView(value)
295
+ ) {
296
+ return true;
297
+ }
298
+
299
+ return (
300
+ typeof SharedArrayBuffer === "function" &&
301
+ value instanceof SharedArrayBuffer
302
+ );
281
303
  }
@@ -6,6 +6,20 @@ import {buildMap} from "../../../source/data/buildmap.mjs";
6
6
 
7
7
  describe('buildMap', function () {
8
8
 
9
+ describe('falsy keys and literal replacements issue #569', function () {
10
+ it('should preserve a zero key returned by a selector callback', function () {
11
+ const value = {label: 'zero'};
12
+ const map = buildMap({}, () => new Map([[0, value]]), '${label}');
13
+ expect([...map]).to.deep.equal([[0, 'zero']]);
14
+ });
15
+
16
+ it('should insert replacement tokens literally', function () {
17
+ const value = {label: '$&-$`-$\''};
18
+ const map = buildMap({}, () => new Map([['key', value]]), '${label}');
19
+ expect(map.get('key')).to.equal('$&-$`-$\'');
20
+ });
21
+ });
22
+
9
23
  let convertMapResult = function (r) {
10
24
  if (r instanceof Map) {
11
25
  r = Object.fromEntries(r);
@@ -170,6 +170,55 @@ describe('buildTree', function () {
170
170
 
171
171
  });
172
172
 
173
+ describe('parent metadata issue #568', function () {
174
+ const createTree = (records) => buildTree(
175
+ records,
176
+ (values) => new Map(values.map((value, index) => [index, value])),
177
+ 'id',
178
+ 'parent',
179
+ );
180
+
181
+ [
182
+ [
183
+ {id: 1, parent: null},
184
+ {id: 2, parent: 1},
185
+ {id: 3, parent: 2},
186
+ ],
187
+ [
188
+ {id: 3, parent: 2},
189
+ {id: 2, parent: 1},
190
+ {id: 1, parent: null},
191
+ ],
192
+ ].forEach((records, index) => {
193
+ it(`should retain immediate parents independent of input order ${index + 1}`, function () {
194
+ const tree = createTree(records);
195
+ const root = [...tree][0];
196
+ const child = [...root.childNodes][0];
197
+ const leaf = [...child.childNodes][0];
198
+
199
+ expect(root.parent).to.equal(null);
200
+ expect(root.level).to.equal(0);
201
+ expect(child.parent).to.equal(root);
202
+ expect(child.level).to.equal(1);
203
+ expect(leaf.parent).to.equal(child);
204
+ expect(leaf.level).to.equal(2);
205
+ });
206
+ });
207
+
208
+ it('should preserve frozen input records and their identity', function () {
209
+ const rootValue = Object.freeze({id: 1, parent: null});
210
+ const childValue = Object.freeze({id: 2, parent: 1});
211
+ const tree = createTree(Object.freeze([childValue, rootValue]));
212
+ const root = [...tree][0];
213
+ const child = [...root.childNodes][0];
214
+
215
+ expect(root.value).to.equal(rootValue);
216
+ expect(child.value).to.equal(childValue);
217
+ expect(Reflect.ownKeys(rootValue)).to.deep.equal(['id', 'parent']);
218
+ expect(Reflect.ownKeys(childValue)).to.deep.equal(['id', 'parent']);
219
+ });
220
+ });
221
+
173
222
  describe('buildTree() test 2', function () {
174
223
 
175
224
  /**
@@ -17,6 +17,8 @@ describe('Pathfinder', function () {
17
17
  `constructor.prototype.${probe}`,
18
18
  ['__proto__', probe],
19
19
  ['constructor', 'prototype', probe],
20
+ [`safe.__proto__`, probe],
21
+ [`safe.constructor.prototype`, probe],
20
22
  ].forEach(function (path) {
21
23
  it(`setVia(${JSON.stringify(path)}) should reject unsafe mutation paths`, function () {
22
24
  try {
@@ -108,6 +110,38 @@ describe('Pathfinder', function () {
108
110
  expect(() => new Pathfinder({values}).setVia('values.next', {}))
109
111
  .to.throw(Error, 'unsupported action for this data type in setValueViaPath');
110
112
  });
113
+
114
+ it('exists() should use collection membership semantics', function () {
115
+ expect(new Pathfinder(new Map([['entry', 0]])).exists('entry')).to.equal(true);
116
+ expect(new Pathfinder(new Map()).exists('entry')).to.equal(false);
117
+ expect(new Pathfinder(new Set([false])).exists('0')).to.equal(true);
118
+ expect(new Pathfinder(new Set()).exists('0')).to.equal(false);
119
+ });
120
+
121
+ it('wildcards should enumerate Map and Set values', function () {
122
+ expect([...new Pathfinder(new Map([['entry', false]])).getVia('*')])
123
+ .to.deep.equal([['entry', false]]);
124
+ expect([...new Pathfinder(new Set(['first', 'second'])).getVia('*')])
125
+ .to.deep.equal([['0', 'first'], ['1', 'second']]);
126
+ });
127
+
128
+ it('collection indices should reject trailing characters', function () {
129
+ expect(() => new Pathfinder(['first', 'second']).getVia('1suffix'))
130
+ .to.throw(TypeError);
131
+ expect(() => new Pathfinder(new Set(['first', 'second'])).getVia('1suffix'))
132
+ .to.throw(TypeError);
133
+ });
134
+
135
+ it('should support null-prototype objects', function () {
136
+ const subject = Object.assign(Object.create(null), {present: false});
137
+ const pathfinder = new Pathfinder(subject);
138
+
139
+ expect(pathfinder.getVia('present')).to.equal(false);
140
+ expect(pathfinder.exists('present')).to.equal(true);
141
+ expect(pathfinder.exists('missing')).to.equal(false);
142
+ pathfinder.setVia('next', 0);
143
+ expect(subject.next).to.equal(0);
144
+ });
111
145
  });
112
146
 
113
147
  let convertMapResult = function (r) {
@@ -8,6 +8,32 @@ import {initJSDOM} from "../../util/jsdom.mjs";
8
8
 
9
9
  describe('Transformer', function () {
10
10
 
11
+ describe('falsy property values issue #569', function () {
12
+ [
13
+ ['key:value:fallback', {value: 0}, 0],
14
+ ['property:value:fallback', {value: false}, false],
15
+ ['index:0:fallback', [''], ''],
16
+ ['key:value:fallback', {value: null}, null],
17
+ ['first-key:fallback', {a: false}, false],
18
+ ['last-key:fallback', {a: 1, z: 0}, 0],
19
+ ].forEach(([definition, value, expected]) => {
20
+ it(`${definition} should preserve ${String(expected)}`, function () {
21
+ expect(new Transformer(definition).run(value)).to.equal(expected);
22
+ });
23
+ });
24
+
25
+ it('should distinguish inherited and missing properties', function () {
26
+ const inherited = Object.create({value: false});
27
+ expect(new Transformer('key:value:fallback').run(inherited)).to.equal(false);
28
+ expect(new Transformer('key:missing:fallback').run(inherited)).to.equal('fallback');
29
+ });
30
+
31
+ it('should support null-prototype objects', function () {
32
+ const value = Object.assign(Object.create(null), {entry: 0});
33
+ expect(new Transformer('key:entry:fallback').run(value)).to.equal(0);
34
+ });
35
+ });
36
+
11
37
  before(function (done) {
12
38
 
13
39
  let promises = []
@@ -384,6 +384,91 @@ describe('DOM', function () {
384
384
  control.remove();
385
385
  });
386
386
 
387
+ it('should not let a stale value attribute overwrite a newer property issue #566', async function () {
388
+ const htmlTAG = 'monster-customcontrol-value-race-566';
389
+
390
+ class ValueRaceComponent extends CustomControl {
391
+ static getTag() {
392
+ return htmlTAG;
393
+ }
394
+
395
+ get defaults() {
396
+ return Object.assign({}, super.defaults, {shadowMode: false});
397
+ }
398
+
399
+ get value() {
400
+ return this._value ?? '';
401
+ }
402
+
403
+ set value(value) {
404
+ this._value = value;
405
+ }
406
+ }
407
+
408
+ registerCustomElement(ValueRaceComponent);
409
+ const control = document.createElement(htmlTAG);
410
+ document.body.appendChild(control);
411
+
412
+ control.setAttribute('value', 'attribute-value');
413
+ control.value = 'newer-property-value';
414
+ await new Promise((resolve) => setTimeout(resolve, 30));
415
+
416
+ expect(control.getAttribute('value')).to.equal('attribute-value');
417
+ expect(control.value).to.equal('newer-property-value');
418
+ control.remove();
419
+ });
420
+
421
+ it('should coalesce rapid value attributes and preserve reflected setters issue #566', async function () {
422
+ const htmlTAG = 'monster-customcontrol-reflected-value-566';
423
+
424
+ class ReflectedValueComponent extends CustomControl {
425
+ static getTag() {
426
+ return htmlTAG;
427
+ }
428
+
429
+ get defaults() {
430
+ return Object.assign({}, super.defaults, {shadowMode: false});
431
+ }
432
+
433
+ get value() {
434
+ return this._value ?? '';
435
+ }
436
+
437
+ set value(value) {
438
+ this._value = String(value ?? '');
439
+ if (this.getAttribute('value') !== this._value) {
440
+ this.setAttribute('value', this._value);
441
+ }
442
+ }
443
+ }
444
+
445
+ registerCustomElement(ReflectedValueComponent);
446
+ const control = document.createElement(htmlTAG);
447
+ document.body.appendChild(control);
448
+ control.setAttribute('value', 'first');
449
+ control.setAttribute('value', 'second');
450
+ await new Promise((resolve, reject) => {
451
+ const deadline = Date.now() + 500;
452
+ const check = () => {
453
+ if (control.value === 'second') {
454
+ resolve();
455
+ } else if (Date.now() >= deadline) {
456
+ reject(new Error('value attribute synchronization timed out'));
457
+ } else {
458
+ setTimeout(check, 5);
459
+ }
460
+ };
461
+ check();
462
+ });
463
+ expect(control.value).to.equal('second');
464
+
465
+ control.value = 'reflected';
466
+ await new Promise((resolve) => setTimeout(resolve, 30));
467
+ expect(control.value).to.equal('reflected');
468
+ expect(control.getAttribute('value')).to.equal('reflected');
469
+ control.remove();
470
+ });
471
+
387
472
  it('should replay a pre-upgrade value once after assembly issue #501', async function () {
388
473
  const htmlTAG = 'monster-customcontrol-pre-upgrade';
389
474
  const control = document.createElement(htmlTAG);
@@ -587,6 +587,60 @@ describe('DOM', function () {
587
587
  linkedUpdater[internalSymbol].subject.detachObserver(observer);
588
588
  }
589
589
  });
590
+
591
+ it('should remove stale updater keys after replacing an option object issue #565', async function () {
592
+ const htmlTAG = 'monster-testclass-object-sync-565';
593
+
594
+ if (!customElements.get(htmlTAG)) {
595
+ class ObjectSyncComponent extends CustomElement {
596
+ static getTag() {
597
+ return htmlTAG;
598
+ }
599
+
600
+ get defaults() {
601
+ return Object.assign({}, super.defaults, {
602
+ settings: {a: 'initial', b: 'retained'},
603
+ templates: {
604
+ main: '<span data-monster-replace="path:settings.a"></span>',
605
+ },
606
+ });
607
+ }
608
+ }
609
+
610
+ registerCustomElement(ObjectSyncComponent);
611
+ }
612
+
613
+ const element = document.createElement(htmlTAG);
614
+ document.getElementById('test1').appendChild(element);
615
+ await new Promise((resolve) => setTimeout(resolve, 20));
616
+
617
+ let linkedUpdater;
618
+ for (const group of element[customElementUpdaterLinkSymbol]) {
619
+ for (const updater of group) {
620
+ linkedUpdater = updater;
621
+ break;
622
+ }
623
+ if (linkedUpdater) break;
624
+ }
625
+ expect(linkedUpdater).to.exist;
626
+
627
+ element.setOptions({settings: {a: 'merged'}});
628
+ await new Promise((resolve) => setTimeout(resolve, 20));
629
+ expect(linkedUpdater.getSubject().settings).to.deep.include({
630
+ a: 'merged',
631
+ b: 'retained',
632
+ });
633
+
634
+ const settingsProxy = linkedUpdater.getSubject().settings;
635
+ element.setOption('settings', {a: 'replacement'});
636
+ await new Promise((resolve) => setTimeout(resolve, 20));
637
+
638
+ expect(linkedUpdater.getSubject().settings).to.equal(settingsProxy);
639
+ expect(linkedUpdater.getSubject().settings).to.deep.equal({
640
+ a: 'replacement',
641
+ });
642
+ expect(element.shadowRoot.textContent).to.equal('replacement');
643
+ });
590
644
  })
591
645
 
592
646
  describe('setOptions()', function () {
@@ -1,14 +1,61 @@
1
1
  import {expect} from 'chai';
2
2
  import {getSlottedNodes} from "../../../source/dom/slotted.mjs";
3
+ import {getSlottedElements} from "../../../source/dom/slotted.mjs";
4
+ import {initJSDOM} from "../../util/jsdom.mjs";
3
5
 
4
6
 
5
7
  describe('getSlottedNodes', () => {
6
8
 
9
+ before(async () => {
10
+ await initJSDOM();
11
+ });
12
+
7
13
 
8
14
  it('should return an empty Set if there is no ShadowRoot', () => {
9
15
  expect(getSlottedNodes).is.a('function');
10
16
  });
11
17
 
18
+ it('should ignore text nodes when applying an element query issue #567', () => {
19
+ const host = document.createElement('div');
20
+ host.append(' whitespace ');
21
+ const child = document.createElement('span');
22
+ child.className = 'match';
23
+ host.appendChild(child);
24
+ host.attachShadow({mode: 'open'}).innerHTML = '<slot></slot>';
25
+ document.body.appendChild(host);
26
+
27
+ expect([...getSlottedNodes.call(host, '.match')]).to.deep.equal([child]);
28
+ host.remove();
29
+ });
30
+
31
+ it('should match empty and spaced slot names without selector interpolation issue #567', () => {
32
+ const host = document.createElement('div');
33
+ const defaultChild = document.createElement('span');
34
+ const namedChild = document.createElement('span');
35
+ namedChild.slot = 'some slot';
36
+ host.append(defaultChild, namedChild);
37
+ host.attachShadow({mode: 'open'}).innerHTML =
38
+ '<slot name=""></slot><slot name="some slot"></slot>';
39
+ document.body.appendChild(host);
40
+
41
+ expect([...getSlottedElements.call(host, undefined, null)]).to.deep.equal([defaultChild]);
42
+ expect([...getSlottedElements.call(host, undefined, 'some slot')]).to.deep.equal([namedChild]);
43
+ host.remove();
44
+ });
45
+
46
+ it('should accept the internally managed closed shadow root issue #567', () => {
47
+ const host = document.createElement('div');
48
+ const child = document.createElement('span');
49
+ host.appendChild(child);
50
+ const root = host.attachShadow({mode: 'closed'});
51
+ root.innerHTML = '<slot></slot>';
52
+ document.body.appendChild(host);
53
+
54
+ expect(host.shadowRoot).to.equal(null);
55
+ expect([...getSlottedElements.call(host, undefined, undefined, root)])
56
+ .to.deep.equal([child]);
57
+ host.remove();
58
+ });
12
59
 
13
- });
14
60
 
61
+ });
@@ -81,6 +81,17 @@ describe('Template', function () {
81
81
  expect(t).is.instanceof(Template);
82
82
 
83
83
  })
84
+
85
+ it('should resolve from a text node context issue #567', function () {
86
+ const container = document.createElement('div');
87
+ const text = document.createTextNode('context');
88
+ container.appendChild(text);
89
+ document.body.appendChild(container);
90
+
91
+ const template = findDocumentTemplate('mytemplate', text);
92
+ expect(template).to.be.instanceof(Template);
93
+ container.remove();
94
+ });
84
95
  });
85
96
 
86
97
  describe('Thmened Template', function () {
@@ -1665,6 +1665,78 @@ describe("DOM", function () {
1665
1665
  .catch((e) => done(new Error(e)));
1666
1666
  });
1667
1667
 
1668
+ it("should not move unchanged keyed nodes issue #564", async function () {
1669
+ const element = document.getElementById("test-patch");
1670
+ const a = document.createElement("span");
1671
+ const b = document.createElement("span");
1672
+ a.dataset.key = "a";
1673
+ b.dataset.key = "b";
1674
+
1675
+ const updater = new Updater(element, {
1676
+ text: "",
1677
+ contentNode: document.createElement("div"),
1678
+ fragmentNode: document.createDocumentFragment(),
1679
+ itemsArray: [],
1680
+ keyedItems: [a, b],
1681
+ keyedDataItems: [],
1682
+ items: [],
1683
+ });
1684
+ updater.setCallback("getPatchKey", (value) => value?.dataset?.key);
1685
+ await updater.run();
1686
+
1687
+ const target = document.getElementById("patch-keyed");
1688
+ let moves = 0;
1689
+ const appendChild = target.appendChild;
1690
+ const insertBefore = target.insertBefore;
1691
+ target.appendChild = function (...args) {
1692
+ moves++;
1693
+ return appendChild.apply(this, args);
1694
+ };
1695
+ target.insertBefore = function (...args) {
1696
+ moves++;
1697
+ return insertBefore.apply(this, args);
1698
+ };
1699
+
1700
+ updater.getSubject().keyedItems = [a, b];
1701
+ await new Promise((resolve) => setTimeout(resolve, 30));
1702
+
1703
+ expect(moves).to.equal(0);
1704
+ expect(Array.from(target.children)).to.deep.equal([a, b]);
1705
+ });
1706
+
1707
+ it("should dispose a keyed node replaced under the same key issue #564", async function () {
1708
+ const element = document.getElementById("test-patch");
1709
+ const first = document.createElement("span");
1710
+ const replacement = document.createElement("span");
1711
+ first.dataset.key = "same";
1712
+ replacement.dataset.key = "same";
1713
+ let disposeCount = 0;
1714
+ addToObjectLink(
1715
+ first,
1716
+ customElementUpdaterLinkSymbol,
1717
+ new Set([{dispose() { disposeCount++; }}]),
1718
+ );
1719
+
1720
+ const updater = new Updater(element, {
1721
+ text: "",
1722
+ contentNode: document.createElement("div"),
1723
+ fragmentNode: document.createDocumentFragment(),
1724
+ itemsArray: [],
1725
+ keyedItems: [first],
1726
+ keyedDataItems: [],
1727
+ items: [],
1728
+ });
1729
+ updater.setCallback("getPatchKey", (value) => value?.dataset?.key);
1730
+ await updater.run();
1731
+
1732
+ updater.getSubject().keyedItems = [replacement];
1733
+ await new Promise((resolve) => setTimeout(resolve, 30));
1734
+
1735
+ expect(disposeCount).to.equal(1);
1736
+ expect(first.isConnected).to.equal(false);
1737
+ expect(document.getElementById("patch-keyed").firstChild).to.equal(replacement);
1738
+ });
1739
+
1668
1740
  it("should remove stale keyed nodes and keep surviving node identity", function (done) {
1669
1741
  let element = document.getElementById("test-patch");
1670
1742
  const a = document.createElement("span");
@@ -2620,6 +2692,52 @@ describe("DOM", function () {
2620
2692
  });
2621
2693
  });
2622
2694
 
2695
+ describe("Updater binding correctness issue #563", function () {
2696
+ it("should ignore unchecked radios during retrieve", function () {
2697
+ const mocks = document.getElementById("mocks");
2698
+ mocks.innerHTML = `
2699
+ <input type="radio" value="selected" checked data-monster-bind="path:choice">
2700
+ <input type="radio" value="unchecked" data-monster-bind="path:choice">
2701
+ `;
2702
+
2703
+ const updater = new Updater(mocks, {choice: "initial"});
2704
+ updater.retrieve();
2705
+
2706
+ expect(updater.getSubject().choice).to.equal("selected");
2707
+ });
2708
+
2709
+ it("should not change select state for non-value attributes", async function () {
2710
+ const mocks = document.getElementById("mocks");
2711
+ mocks.innerHTML = `
2712
+ <select id="binding-select" data-monster-attributes="title path:title">
2713
+ <option value="a">a</option>
2714
+ <option value="b" selected>b</option>
2715
+ </select>
2716
+ `;
2717
+
2718
+ const updater = new Updater(mocks, {title: "label"});
2719
+ await updater.run();
2720
+
2721
+ const select = document.getElementById("binding-select");
2722
+ expect(select.title).to.equal("label");
2723
+ expect(select.value).to.equal("b");
2724
+ });
2725
+
2726
+ it("should evaluate static attribute and property commands", async function () {
2727
+ const mocks = document.getElementById("mocks");
2728
+ mocks.innerHTML = `
2729
+ <div id="static-attribute" data-monster-attributes="title static:hello"></div>
2730
+ <input id="static-property" data-monster-properties="value static:world">
2731
+ `;
2732
+
2733
+ const updater = new Updater(mocks, {trigger: true});
2734
+ await updater.run();
2735
+
2736
+ expect(document.getElementById("static-attribute").title).to.equal("hello");
2737
+ expect(document.getElementById("static-property").value).to.equal("world");
2738
+ });
2739
+ });
2740
+
2623
2741
  describe("Updater pipe failure recovery issue #498", function () {
2624
2742
  const waitForUpdate = () => new Promise((resolve) => setTimeout(resolve, 20));
2625
2743
 
@@ -241,6 +241,40 @@ describe('ProxyObserver', function () {
241
241
  proxy.attachObserver(observer);
242
242
  delete proxy.getSubject().a;
243
243
  });
244
+
245
+ it('should ignore missing and inherited deletes without notifying issue #570', async function () {
246
+ const prototype = {inherited: true};
247
+ const value = Object.create(prototype);
248
+ const proxy = new ProxyObserver(value);
249
+ let notifications = 0;
250
+ proxy.attachObserver(new Observer(function () {
251
+ notifications++;
252
+ }));
253
+
254
+ expect(() => delete proxy.getSubject().missing).to.not.throw();
255
+ expect(() => delete proxy.getSubject().inherited).to.not.throw();
256
+ await new Promise((resolve) => setTimeout(resolve, 0));
257
+
258
+ expect(proxy.getSubject().inherited).to.equal(true);
259
+ expect(notifications).to.equal(0);
260
+ });
261
+
262
+ it('should report failed non-configurable deletes without notifying issue #570', async function () {
263
+ const value = {};
264
+ Object.defineProperty(value, 'fixed', {
265
+ configurable: false,
266
+ value: true,
267
+ });
268
+ const proxy = new ProxyObserver(value);
269
+ let notifications = 0;
270
+ proxy.attachObserver(new Observer(function () {
271
+ notifications++;
272
+ }));
273
+
274
+ expect(Reflect.deleteProperty(proxy.getSubject(), 'fixed')).to.equal(false);
275
+ await new Promise((resolve) => setTimeout(resolve, 0));
276
+ expect(notifications).to.equal(0);
277
+ });
244
278
  });
245
279
 
246
280
  describe('symbol properties', function () {
@@ -342,6 +376,47 @@ describe('ProxyObserver', function () {
342
376
  expect(proxy.getSubject().map).to.equal(replacement);
343
377
  expect(notifications).to.equal(1);
344
378
  });
379
+
380
+ it('should obey frozen-property proxy invariants issue #570', function () {
381
+ const nested = {value: true};
382
+ const frozen = Object.freeze({nested});
383
+ const subject = new ProxyObserver({frozen}).getSubject();
384
+
385
+ expect(() => subject.frozen.nested).to.not.throw();
386
+ expect(subject.frozen.nested).to.equal(nested);
387
+ });
388
+
389
+ it('should preserve confirmed internal-slot native objects issue #570', async function () {
390
+ const weakKey = {};
391
+ const regexp = /monster/g;
392
+ const buffer = new ArrayBuffer(8);
393
+ const view = new DataView(buffer);
394
+ const typed = new Uint8Array(buffer);
395
+ const weakMap = new WeakMap([[weakKey, 'value']]);
396
+ const weakSet = new WeakSet([weakKey]);
397
+ const promise = Promise.resolve('resolved');
398
+ const subject = new ProxyObserver({
399
+ regexp,
400
+ buffer,
401
+ view,
402
+ typed,
403
+ weakMap,
404
+ weakSet,
405
+ promise,
406
+ }).getSubject();
407
+
408
+ expect(subject.regexp).to.equal(regexp);
409
+ expect(subject.regexp.test('monster')).to.equal(true);
410
+ expect(subject.buffer).to.equal(buffer);
411
+ expect(subject.buffer.byteLength).to.equal(8);
412
+ expect(subject.view).to.equal(view);
413
+ expect(subject.view.byteLength).to.equal(8);
414
+ expect(subject.typed).to.equal(typed);
415
+ expect(subject.typed.length).to.equal(8);
416
+ expect(subject.weakMap.get(weakKey)).to.equal('value');
417
+ expect(subject.weakSet.has(weakKey)).to.equal(true);
418
+ expect(await subject.promise).to.equal('resolved');
419
+ });
345
420
  });
346
421
 
347
422
  })