@schukai/monster 4.153.0 → 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 (52) 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 +6 -5
  9. package/source/data/buildtree.mjs +0 -8
  10. package/source/data/datasource.mjs +3 -10
  11. package/source/data/diff.mjs +13 -12
  12. package/source/data/extend.mjs +23 -1
  13. package/source/data/pathfinder.mjs +228 -32
  14. package/source/data/transformer.mjs +2 -2
  15. package/source/dom/customcontrol.mjs +35 -9
  16. package/source/dom/customelement.mjs +293 -31
  17. package/source/dom/slotted.mjs +46 -31
  18. package/source/dom/template.mjs +4 -1
  19. package/source/dom/updater.mjs +57 -17
  20. package/source/types/base.mjs +7 -6
  21. package/source/types/basewithoptions.mjs +1 -8
  22. package/source/types/internal.mjs +4 -10
  23. package/source/types/node.mjs +1 -3
  24. package/source/types/observerlist.mjs +9 -17
  25. package/source/types/proxyobserver.mjs +51 -12
  26. package/source/types/tokenlist.mjs +13 -8
  27. package/source/util/clone.mjs +44 -14
  28. package/test/cases/browser-bundle.mjs +75 -0
  29. package/test/cases/components/time/month-calendar.mjs +115 -0
  30. package/test/cases/data/buildmap.mjs +14 -0
  31. package/test/cases/data/buildtree.mjs +49 -0
  32. package/test/cases/data/diff.mjs +86 -0
  33. package/test/cases/data/extend.mjs +52 -0
  34. package/test/cases/data/pathfinder-resolution-baseline.mjs +322 -0
  35. package/test/cases/data/pathfinder.mjs +163 -0
  36. package/test/cases/data/transformer.mjs +26 -0
  37. package/test/cases/dom/customcontrol.mjs +85 -0
  38. package/test/cases/dom/customelement-rebind-baseline.mjs +323 -0
  39. package/test/cases/dom/customelement-teardown.mjs +408 -0
  40. package/test/cases/dom/customelement.mjs +408 -0
  41. package/test/cases/dom/dimension.mjs +34 -10
  42. package/test/cases/dom/slotted-nodes.mjs +48 -1
  43. package/test/cases/dom/template.mjs +11 -0
  44. package/test/cases/dom/updater-remove.mjs +198 -0
  45. package/test/cases/dom/updater.mjs +118 -0
  46. package/test/cases/packaging.mjs +213 -0
  47. package/test/cases/types/base.mjs +50 -0
  48. package/test/cases/types/observerlist.mjs +103 -1
  49. package/test/cases/types/proxyobserver.mjs +189 -0
  50. package/test/cases/types/regex.mjs +4 -0
  51. package/test/cases/types/tokenlist.mjs +64 -1
  52. package/test/cases/util/clone.mjs +115 -0
@@ -651,7 +651,7 @@ function retrieveAndSetValue(element) {
651
651
 
652
652
  let options = element?.selectedOptions;
653
653
  if (options === undefined)
654
- options = element.querySelectorAll(":scope option:checked");
654
+ options = element.querySelectorAll("option:checked");
655
655
  value = Array.from(options).map(({ value }) => value);
656
656
 
657
657
  break;
@@ -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
@@ -830,8 +847,12 @@ function removeElement(_change, renderContext) {
830
847
  }
831
848
 
832
849
  let changed = false;
850
+ // No :scope prefix: querySelectorAll() on an element already returns only its
851
+ // descendants, so the prefix carries no meaning here, while :scope does not
852
+ // resolve for shadow root content under jsdom, which made this a silent no-op
853
+ // for every component that renders into a shadow root.
833
854
  for (const [, element] of this[internalSymbol].element
834
- .querySelectorAll(`:scope [${ATTRIBUTE_UPDATER_REMOVE}]`)
855
+ .querySelectorAll(`[${ATTRIBUTE_UPDATER_REMOVE}]`)
835
856
  .entries()) {
836
857
  teardownManagedSubtree(element);
837
858
  element.parentNode.removeChild(element);
@@ -1201,7 +1222,10 @@ function getSlots(container, cache) {
1201
1222
  return cache.get(container);
1202
1223
  }
1203
1224
 
1204
- const slots = container.querySelectorAll("slot");
1225
+ const slots = Array.from(container.querySelectorAll("slot"));
1226
+ if (container.localName === "slot") {
1227
+ slots.unshift(container);
1228
+ }
1205
1229
  cache.set(container, slots);
1206
1230
  return slots;
1207
1231
  }
@@ -1565,6 +1589,9 @@ function replaceKeyedPatchedChildren(
1565
1589
  const nextNode = preparePatchedNode(reused, renderedValue);
1566
1590
  nextNode[patchNodeKeySymbol] = normalizedKey;
1567
1591
  if (nextNode !== reused && reused.parentNode === element) {
1592
+ if (reused instanceof HTMLElement) {
1593
+ teardownManagedSubtree(reused);
1594
+ }
1568
1595
  element.replaceChild(nextNode, reused);
1569
1596
  }
1570
1597
  nextNodes.push(nextNode);
@@ -1591,15 +1618,11 @@ function replaceKeyedPatchedChildren(
1591
1618
  }
1592
1619
  }
1593
1620
 
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
- }
1621
+ for (let index = 0; index < nextNodes.length; index++) {
1622
+ const node = nextNodes[index];
1623
+ const current = element.childNodes[index] ?? null;
1624
+ if (current === node) continue;
1625
+ element.insertBefore(node, current);
1603
1626
  }
1604
1627
  }
1605
1628
 
@@ -1851,8 +1874,7 @@ function runUpdateAttributes(
1851
1874
  if (
1852
1875
  !element.hasAttribute(ATTRIBUTE_UPDATER_SELECT_THIS) &&
1853
1876
  !attributes.includes(`path:${current}`) &&
1854
- !attributes.startsWith("static:") &&
1855
- !attributes.startsWith("i18n:")
1877
+ !hasUnconditionalBinding(attributes)
1856
1878
  ) {
1857
1879
  continue;
1858
1880
  }
@@ -1933,8 +1955,7 @@ function runUpdateProperties(
1933
1955
  if (
1934
1956
  !element.hasAttribute(ATTRIBUTE_UPDATER_SELECT_THIS) &&
1935
1957
  !properties.includes(`path:${current}`) &&
1936
- !properties.startsWith("static:") &&
1937
- !properties.startsWith("i18n:")
1958
+ !hasUnconditionalBinding(properties)
1938
1959
  ) {
1939
1960
  continue;
1940
1961
  }
@@ -1981,6 +2002,9 @@ function runUpdateProperties(
1981
2002
 
1982
2003
  function handleInputControlAttributeUpdate(element, name, value) {
1983
2004
  if (element instanceof HTMLSelectElement) {
2005
+ if (name !== "value") {
2006
+ return;
2007
+ }
1984
2008
  switch (element.type) {
1985
2009
  case "select-multiple":
1986
2010
  const selectedValues = isArray(value)
@@ -2054,6 +2078,22 @@ function handleInputControlAttributeUpdate(element, name, value) {
2054
2078
  }
2055
2079
  }
2056
2080
 
2081
+ /**
2082
+ * Check whether an attribute/property definition contains a command that is
2083
+ * independent from the changed subject path.
2084
+ * @param {string} definitions
2085
+ * @return {boolean}
2086
+ * @private
2087
+ */
2088
+ function hasUnconditionalBinding(definitions) {
2089
+ return definitions.split(",").some((definition) => {
2090
+ const separator = definition.indexOf(" ");
2091
+ if (separator < 0) return false;
2092
+ const command = trimSpaces(definition.substring(separator + 1));
2093
+ return command.startsWith("static:") || command.startsWith("i18n:");
2094
+ });
2095
+ }
2096
+
2057
2097
  /**
2058
2098
  * @private
2059
2099
  * @param {HTMLElement|*} element
@@ -115,21 +115,22 @@ function checkInstanceSymbol(obj) {
115
115
  }
116
116
 
117
117
  if (proto.hasOwnProperty(instanceSymbol) !== true) {
118
- return checkInstanceSymbol.apply(this, [obj.__proto__]);
118
+ return checkInstanceSymbol.apply(this, [Object.getPrototypeOf(obj)]);
119
119
  }
120
120
 
121
121
  const symbol = proto[instanceSymbol];
122
122
  if (symbol === undefined) {
123
- if (obj.__proto__) {
124
- return checkInstanceSymbol(obj.__proto__);
125
- } else {
126
- return false;
123
+ const parent = Object.getPrototypeOf(obj);
124
+ if (parent) {
125
+ return checkInstanceSymbol.apply(this, [parent]);
127
126
  }
127
+
128
+ return false;
128
129
  }
129
130
 
130
131
  if (symbol === this[instanceSymbol]) {
131
132
  return true;
132
133
  }
133
134
 
134
- return checkInstanceSymbol.apply(this, [obj.__proto__]);
135
+ return checkInstanceSymbol.apply(this, [Object.getPrototypeOf(obj)]);
135
136
  }
@@ -80,13 +80,6 @@ class BaseWithOptions extends Base {
80
80
  * @since 1.10.0
81
81
  */
82
82
  getOption(path, defaultValue) {
83
- let value;
84
-
85
- try {
86
- value = new Pathfinder(this[internalSymbol]).getVia(path);
87
- } catch (e) {}
88
-
89
- if (value === undefined) return defaultValue;
90
- return value;
83
+ return new Pathfinder(this[internalSymbol]).getVia(path, defaultValue);
91
84
  }
92
85
  }
@@ -126,16 +126,10 @@ function equipWithInternal() {
126
126
  * @return {*}
127
127
  */
128
128
  this["getInternal"] = (path, defaultValue) => {
129
- let value;
130
-
131
- try {
132
- value = new Pathfinder(this[internalSymbol].getRealSubject()).getVia(
133
- path,
134
- );
135
- } catch (e) {}
136
-
137
- if (value === undefined) return defaultValue;
138
- return value;
129
+ return new Pathfinder(this[internalSymbol].getRealSubject()).getVia(
130
+ path,
131
+ defaultValue,
132
+ );
139
133
  };
140
134
  }
141
135
 
@@ -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
  }
@@ -55,9 +55,9 @@ class ObserverList extends Base {
55
55
  detach(observer) {
56
56
  validateInstance(observer, Observer);
57
57
 
58
- var i = 0;
59
- var l = this.observers.length;
60
- for (; i < l; i++) {
58
+ // Iterate backwards: splicing while moving forward skips the entry that
59
+ // takes the freed slot, which left adjacent duplicates attached.
60
+ for (let i = this.observers.length - 1; i >= 0; i--) {
61
61
  if (this.observers[i] === observer) {
62
62
  this.observers.splice(i, 1);
63
63
  }
@@ -74,14 +74,8 @@ class ObserverList extends Base {
74
74
  */
75
75
  contains(observer) {
76
76
  validateInstance(observer, Observer);
77
- var i = 0;
78
- var l = this.observers.length;
79
- for (; i < l; i++) {
80
- if (this.observers[i] === observer) {
81
- return true;
82
- }
83
- }
84
- return false;
77
+
78
+ return this.observers.includes(observer);
85
79
  }
86
80
 
87
81
  /**
@@ -90,14 +84,12 @@ class ObserverList extends Base {
90
84
  * @return {Promise}
91
85
  */
92
86
  notify(subject) {
93
- const pomises = [];
87
+ const promises = [];
94
88
 
95
- let i = 0;
96
- const l = this.observers.length;
97
- for (; i < l; i++) {
98
- pomises.push(this.observers[i].update(subject));
89
+ for (const observer of this.observers) {
90
+ promises.push(observer.update(subject));
99
91
  }
100
92
 
101
- return Promise.all(pomises);
93
+ return Promise.all(promises);
102
94
  }
103
95
  }
@@ -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;
@@ -209,7 +218,7 @@ function getHandler() {
209
218
  current = proxy.proxyMap.get(current);
210
219
  }
211
220
 
212
- if (current === value) {
221
+ if (isSameValue(current, value)) {
213
222
  return true;
214
223
  }
215
224
 
@@ -223,7 +232,7 @@ function getHandler() {
223
232
  if (proxy.proxyMap.has(next)) {
224
233
  next = proxy.proxyMap.get(next);
225
234
  }
226
- if (next !== current) {
235
+ if (!isSameValue(next, current)) {
227
236
  proxy.observers.notify(proxy);
228
237
  }
229
238
  }
@@ -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
@@ -269,6 +274,23 @@ function getHandler() {
269
274
  return handler;
270
275
  }
271
276
 
277
+ /**
278
+ * Two NaN values are the same value, so assigning NaN over NaN is not a change.
279
+ * Object.is() is not used here because it would additionally treat 0 and -0 as
280
+ * a change and notify for assignments that do not alter the subject.
281
+ * @param {*} a
282
+ * @param {*} b
283
+ * @return {boolean}
284
+ * @private
285
+ */
286
+ function isSameValue(a, b) {
287
+ if (a === b) {
288
+ return true;
289
+ }
290
+
291
+ return Number.isNaN(a) && Number.isNaN(b);
292
+ }
293
+
272
294
  /**
273
295
  * Native objects with internal slots cannot be used through a transparent Proxy
274
296
  * because their prototype methods require the original receiver.
@@ -277,5 +299,22 @@ function getHandler() {
277
299
  * @private
278
300
  */
279
301
  function isNativeObjectWithInternalSlots(value) {
280
- return value instanceof Date || value instanceof Map || value instanceof Set;
302
+ if (
303
+ value instanceof Date ||
304
+ value instanceof RegExp ||
305
+ value instanceof Map ||
306
+ value instanceof Set ||
307
+ value instanceof WeakMap ||
308
+ value instanceof WeakSet ||
309
+ value instanceof Promise ||
310
+ value instanceof ArrayBuffer ||
311
+ ArrayBuffer.isView(value)
312
+ ) {
313
+ return true;
314
+ }
315
+
316
+ return (
317
+ typeof SharedArrayBuffer === "function" &&
318
+ value instanceof SharedArrayBuffer
319
+ );
281
320
  }
@@ -84,13 +84,18 @@ class TokenList extends Base {
84
84
  */
85
85
  contains(value) {
86
86
  if (isString(value)) {
87
- value = value.trim();
88
- let counter = 0;
89
- value.split(" ").forEach((token) => {
90
- if (this.tokens.has(token.trim()) === false) return false;
91
- counter++;
92
- });
93
- return counter > 0 ? true : false;
87
+ // Repeated or surrounding whitespace produces empty segments, which are
88
+ // not tokens. A value without any token contains nothing.
89
+ const tokens = value
90
+ .split(" ")
91
+ .map((token) => token.trim())
92
+ .filter((token) => token !== "");
93
+
94
+ if (tokens.length === 0) {
95
+ return false;
96
+ }
97
+
98
+ return tokens.every((token) => this.tokens.has(token));
94
99
  }
95
100
 
96
101
  if (isIterable(value)) {
@@ -100,7 +105,7 @@ class TokenList extends Base {
100
105
  if (this.tokens.has(token.trim()) === false) return false;
101
106
  counter++;
102
107
  }
103
- return counter > 0 ? true : false;
108
+ return counter > 0;
104
109
  }
105
110
 
106
111
  return false;
@@ -42,6 +42,16 @@ export { clone };
42
42
  * @throws {Error} unable to clone obj! its type isn't supported.
43
43
  */
44
44
  function clone(obj) {
45
+ return cloneValue(obj, new WeakMap());
46
+ }
47
+
48
+ /**
49
+ * @private
50
+ * @param {*} obj
51
+ * @param {WeakMap} seen already cloned objects, keyed by source
52
+ * @return {*}
53
+ */
54
+ function cloneValue(obj, seen) {
45
55
  // typeof null results in 'object'. https://2ality.com/2013/10/typeof-null.html
46
56
  if (null === obj) {
47
57
  return obj;
@@ -59,9 +69,14 @@ function clone(obj) {
59
69
 
60
70
  // Handle Array
61
71
  if (isArray(obj)) {
72
+ if (seen.has(obj)) {
73
+ return seen.get(obj);
74
+ }
75
+
62
76
  const copy = [];
77
+ seen.set(obj, copy);
63
78
  for (let i = 0, len = obj.length; i < len; i++) {
64
- copy[i] = clone(obj[i]);
79
+ copy[i] = cloneValue(obj[i], seen);
65
80
  }
66
81
 
67
82
  return copy;
@@ -87,18 +102,28 @@ function clone(obj) {
87
102
 
88
103
  // Handle Map
89
104
  if (obj instanceof Map) {
105
+ if (seen.has(obj)) {
106
+ return seen.get(obj);
107
+ }
108
+
90
109
  const copy = new Map();
110
+ seen.set(obj, copy);
91
111
  for (const [key, value] of obj.entries()) {
92
- copy.set(clone(key), clone(value));
112
+ copy.set(cloneValue(key, seen), cloneValue(value, seen));
93
113
  }
94
114
  return copy;
95
115
  }
96
116
 
97
117
  // Handle Set
98
118
  if (obj instanceof Set) {
119
+ if (seen.has(obj)) {
120
+ return seen.get(obj);
121
+ }
122
+
99
123
  const copy = new Set();
124
+ seen.set(obj, copy);
100
125
  for (const value of obj.values()) {
101
- copy.add(clone(value));
126
+ copy.add(cloneValue(value, seen));
102
127
  }
103
128
  return copy;
104
129
  }
@@ -119,7 +144,7 @@ function clone(obj) {
119
144
  if (typeof navigator !== "undefined" && obj === navigator) return obj;
120
145
  if (typeof JSON !== "undefined" && obj === JSON) return obj;
121
146
  if (isProxy(obj)) return obj; // Handle Proxy-Object
122
- return cloneObject(obj);
147
+ return cloneObject(obj, seen);
123
148
  }
124
149
 
125
150
  throw new Error("unable to clone obj! its type isn't supported.");
@@ -128,12 +153,17 @@ function clone(obj) {
128
153
  /**
129
154
  *
130
155
  * @param {object} obj
156
+ * @param {WeakMap} seen already cloned objects, keyed by source
131
157
  * @return {object}
132
158
  * @private
133
159
  */
134
- function cloneObject(obj) {
160
+ function cloneObject(obj, seen) {
135
161
  validateObject(obj);
136
162
 
163
+ if (seen.has(obj)) {
164
+ return seen.get(obj);
165
+ }
166
+
137
167
  const fkt = obj?.["constructor"];
138
168
  const sourcePrototype = Object.getPrototypeOf(obj);
139
169
 
@@ -145,19 +175,19 @@ function cloneObject(obj) {
145
175
  Object.prototype.hasOwnProperty.call(prototype, "getClone") &&
146
176
  typeOf(obj.getClone) === "function"
147
177
  ) {
178
+ // deliberately before the memo is written: the object decides for
179
+ // itself what its copy is, and that result must not be replaced by
180
+ // a half built copy when the object is reached again inside a cycle.
148
181
  return obj.getClone();
149
182
  }
150
183
  }
151
184
  }
152
185
 
153
- let copy = sourcePrototype === null ? Object.create(null) : {};
154
- if (
155
- sourcePrototype !== null &&
156
- typeof fkt === "function" &&
157
- typeof fkt.call === "function"
158
- ) {
159
- copy = new fkt();
160
- }
186
+ // The constructor is not invoked. Calling it would run arbitrary code and
187
+ // fail for every class whose constructor requires arguments. Classes that
188
+ // need constructor work expose getClone() instead.
189
+ const copy = Object.create(sourcePrototype);
190
+ seen.set(obj, copy);
161
191
 
162
192
  for (const key in obj) {
163
193
  if (!Object.prototype.hasOwnProperty.call(obj, key)) {
@@ -169,7 +199,7 @@ function cloneObject(obj) {
169
199
  continue;
170
200
  }
171
201
 
172
- copy[key] = clone(obj[key]);
202
+ copy[key] = cloneValue(obj[key], seen);
173
203
  }
174
204
 
175
205
  return copy;
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+
3
+ import { expect } from "chai";
4
+ import { buildCurrentTests } from "../web/current-bundle.mjs";
5
+
6
+ /**
7
+ * Guards the browser test bundle for issue #577.
8
+ *
9
+ * The browser tier bundles the cases as bare `import "<file>"` statements. A
10
+ * `sideEffects` list in package.json is a restriction: every path it does not
11
+ * match counts as side effect free, and esbuild then drops those bare imports.
12
+ * With `./test/**` missing from the list the bundle collapsed from about 9 MB
13
+ * to 15 bytes, and all three browser jobs reported `0 passing, 0 failing,
14
+ * 0 pending` rather than failing loudly.
15
+ *
16
+ * This file runs esbuild over the repository, so it is listed in nodeOnlyCases
17
+ * in test/web/current-bundle.mjs and does not run in the browser bundle itself.
18
+ */
19
+ describe("Browser test bundle issue #577", function () {
20
+ this.timeout(60000);
21
+
22
+ let bundle;
23
+
24
+ before(async function () {
25
+ bundle = await buildCurrentTests();
26
+ });
27
+
28
+ it("should collect the case files", function () {
29
+ expect(bundle.files).to.be.an("array");
30
+ expect(
31
+ bundle.files.length,
32
+ "far fewer case files than the repository contains",
33
+ ).to.be.above(150);
34
+ });
35
+
36
+ it("should produce a bundle that actually contains the cases", function () {
37
+ // an emptied bundle is a few bytes, a complete one is megabytes
38
+ expect(
39
+ bundle.code.length,
40
+ "the bundled browser suite is far too small to hold the test cases",
41
+ ).to.be.above(1_000_000);
42
+ });
43
+
44
+ it("should keep the suites of the bundled files", function () {
45
+ // every case file registers suites at module scope, so dropping the bare
46
+ // imports removes these calls even though the file list stays complete
47
+ const suites = [...bundle.code.matchAll(/\bdescribe\(/g)].length;
48
+
49
+ expect(suites, "the bundle registers almost no suites").to.be.above(300);
50
+ });
51
+
52
+ it("should contain content from several distinct case files", function () {
53
+ for (const marker of [
54
+ "Pathfinder resolution baseline",
55
+ "CustomElement rebind baseline",
56
+ "Updater remove attribute",
57
+ "MonthCalendar date configuration",
58
+ "contains all tokens",
59
+ ]) {
60
+ expect(
61
+ bundle.code.includes(marker),
62
+ `the bundle does not contain "${marker}", so cases are missing`,
63
+ ).to.be.true;
64
+ }
65
+ });
66
+
67
+ it("should not bundle the node only cases", function () {
68
+ for (const nodeOnly of ["packaging.mjs", "browser-bundle.mjs"]) {
69
+ expect(
70
+ bundle.files.some((file) => file.endsWith(`test/cases/${nodeOnly}`)),
71
+ `${nodeOnly} must stay out of the browser bundle`,
72
+ ).to.be.false;
73
+ }
74
+ });
75
+ });