@schukai/monster 4.153.1 → 4.154.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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
package/source/util/clone.mjs
CHANGED
|
@@ -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] =
|
|
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(
|
|
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(
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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] =
|
|
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
|
+
});
|
|
@@ -70,6 +70,121 @@ describe("MonthCalendar date configuration", () => {
|
|
|
70
70
|
expect(currentDays(calendar)[0].month).to.equal(new Date().getMonth() + 1);
|
|
71
71
|
expect(calendar.getOption("calendarDays")).to.have.length(42);
|
|
72
72
|
});
|
|
73
|
+
describe("document language issue #578", () => {
|
|
74
|
+
// The oracle is Intl itself with an explicit tag, so none of these
|
|
75
|
+
// assertions depend on the locale of the machine running the suite.
|
|
76
|
+
const MONDAY = new Date(1970, 0, 5);
|
|
77
|
+
const weekdayIn = (tag, format) =>
|
|
78
|
+
new Intl.DateTimeFormat(tag, { weekday: format }).format(MONDAY);
|
|
79
|
+
|
|
80
|
+
let originalLang;
|
|
81
|
+
|
|
82
|
+
beforeEach(() => {
|
|
83
|
+
originalLang = document.documentElement.getAttribute("lang");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
afterEach(() => {
|
|
87
|
+
if (originalLang === null) {
|
|
88
|
+
document.documentElement.removeAttribute("lang");
|
|
89
|
+
} else {
|
|
90
|
+
document.documentElement.setAttribute("lang", originalLang);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
async function calendarFor(lang, format = "long") {
|
|
95
|
+
document.documentElement.setAttribute("lang", lang);
|
|
96
|
+
const calendar = document.createElement("monster-month-calendar");
|
|
97
|
+
calendar.setOption("startDate", "2026-10-01");
|
|
98
|
+
document.getElementById("mocks").append(calendar);
|
|
99
|
+
await updates();
|
|
100
|
+
calendar.setOption("locale.weekdayFormat", format);
|
|
101
|
+
await updates();
|
|
102
|
+
return calendar;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// net: the weekday header is always seven entries, indexed Monday first,
|
|
106
|
+
// and every label is a non empty string
|
|
107
|
+
it("produces seven indexed weekdays", async () => {
|
|
108
|
+
const calendar = await calendarFor("en");
|
|
109
|
+
const weekdays = calendar.getOption("calendarWeekdays");
|
|
110
|
+
|
|
111
|
+
expect(weekdays).to.have.length(7);
|
|
112
|
+
expect(weekdays.map((entry) => entry.index)).to.deep.equal([
|
|
113
|
+
0, 1, 2, 3, 4, 5, 6,
|
|
114
|
+
]);
|
|
115
|
+
for (const entry of weekdays) {
|
|
116
|
+
expect(entry.label).to.be.a("string").and.to.have.length.above(0);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("rejects an invalid weekday format and falls back to short", async () => {
|
|
121
|
+
const calendar = await calendarFor("en", "bogus");
|
|
122
|
+
const weekdays = calendar.getOption("calendarWeekdays");
|
|
123
|
+
|
|
124
|
+
expect(calendar.hasAttribute(ATTRIBUTE_ERRORMESSAGE)).to.be.true;
|
|
125
|
+
// the short form is shorter than the long one, whatever the language
|
|
126
|
+
expect(weekdays[0].label.length).to.be.below(
|
|
127
|
+
weekdayIn("en", "long").length,
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// net: this path reads locale.language, a string, and already works
|
|
132
|
+
it("translates the more-appointments label from the document language", async () => {
|
|
133
|
+
const calendar = await calendarFor("de");
|
|
134
|
+
const cell = calendar.shadowRoot.querySelector(
|
|
135
|
+
'[data-monster-day="2026-10-01"]',
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
expect(
|
|
139
|
+
cell.querySelector('[data-monster-role="more-appointments"]')
|
|
140
|
+
.textContent,
|
|
141
|
+
).to.contain("Weitere Termine");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// regression: Intl received the Locale instance instead of its tag, so the
|
|
145
|
+
// document language was ignored and the host system locale decided. At
|
|
146
|
+
// most one of these languages can match the machine locale, so the set is
|
|
147
|
+
// red before the fix in any environment.
|
|
148
|
+
for (const lang of ["ja", "de", "en", "es"]) {
|
|
149
|
+
it(`formats weekday names for lang="${lang}"`, async () => {
|
|
150
|
+
const calendar = await calendarFor(lang);
|
|
151
|
+
|
|
152
|
+
expect(calendar.getOption("calendarWeekdays")[0].label).to.equal(
|
|
153
|
+
weekdayIn(lang, "long"),
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
it("changes weekday names when the document language changes", async () => {
|
|
159
|
+
const japanese = await calendarFor("ja");
|
|
160
|
+
const japaneseLabel = japanese.getOption("calendarWeekdays")[0].label;
|
|
161
|
+
|
|
162
|
+
const spanish = await calendarFor("es");
|
|
163
|
+
const spanishLabel = spanish.getOption("calendarWeekdays")[0].label;
|
|
164
|
+
|
|
165
|
+
expect(japaneseLabel).to.equal(weekdayIn("ja", "long"));
|
|
166
|
+
expect(spanishLabel).to.equal(weekdayIn("es", "long"));
|
|
167
|
+
expect(japaneseLabel).to.not.equal(spanishLabel);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("falls back to the short format in the document language", async () => {
|
|
171
|
+
const calendar = await calendarFor("ja", "bogus");
|
|
172
|
+
|
|
173
|
+
expect(calendar.hasAttribute(ATTRIBUTE_ERRORMESSAGE)).to.be.true;
|
|
174
|
+
expect(calendar.getOption("calendarWeekdays")[0].label).to.equal(
|
|
175
|
+
weekdayIn("ja", "short"),
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("honors the document language for the short format as well", async () => {
|
|
180
|
+
const calendar = await calendarFor("ja", "short");
|
|
181
|
+
|
|
182
|
+
expect(calendar.getOption("calendarWeekdays")[0].label).to.equal(
|
|
183
|
+
weekdayIn("ja", "short"),
|
|
184
|
+
);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
73
188
|
it("keeps day interactions and weekday updates working after changing months", async () => {
|
|
74
189
|
const calendar = document.createElement("monster-month-calendar");
|
|
75
190
|
document.getElementById("mocks").append(calendar);
|
package/test/cases/data/diff.mjs
CHANGED
|
@@ -285,4 +285,90 @@ describe('Diff', function () {
|
|
|
285
285
|
|
|
286
286
|
});
|
|
287
287
|
|
|
288
|
+
describe('numeric identity issue #571', function () {
|
|
289
|
+
|
|
290
|
+
// net: these describe behaviour that is already correct today
|
|
291
|
+
it('should report an update when NaN is replaced by a number', function () {
|
|
292
|
+
let d = diff({a: NaN}, {a: 1});
|
|
293
|
+
expect(d.length).is.equal(1);
|
|
294
|
+
expect(d[0].operator).is.equal('update');
|
|
295
|
+
expect(d[0].path).is.eql(['a']);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('should report an update when a number is replaced by NaN', function () {
|
|
299
|
+
let d = diff({a: 1}, {a: NaN});
|
|
300
|
+
expect(d.length).is.equal(1);
|
|
301
|
+
expect(d[0].operator).is.equal('update');
|
|
302
|
+
expect(d[0].path).is.eql(['a']);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('should keep treating 0 and -0 as equal', function () {
|
|
306
|
+
expect(JSON.stringify(diff({a: 0}, {a: -0}))).is.equal('[]');
|
|
307
|
+
expect(JSON.stringify(diff({a: -0}, {a: 0}))).is.equal('[]');
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// regression: two equal NaN values are not a change
|
|
311
|
+
it('should not report a change for two NaN values at the same path', function () {
|
|
312
|
+
expect(JSON.stringify(diff({a: NaN}, {a: NaN}))).is.equal('[]');
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('should not report a change for nested NaN values', function () {
|
|
316
|
+
let first = {a: {b: NaN}, c: {d: {e: NaN}}};
|
|
317
|
+
let second = {a: {b: NaN}, c: {d: {e: NaN}}};
|
|
318
|
+
expect(JSON.stringify(diff(first, second))).is.equal('[]');
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('should not report a change for NaN inside an unchanged array', function () {
|
|
322
|
+
expect(JSON.stringify(diff([NaN, 1], [NaN, 1]))).is.equal('[]');
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('should not report a change for a NaN value reached through a wildcard shaped tree', function () {
|
|
326
|
+
let first = {rows: [{value: NaN}, {value: 2}]};
|
|
327
|
+
let second = {rows: [{value: NaN}, {value: 2}]};
|
|
328
|
+
expect(JSON.stringify(diff(first, second))).is.equal('[]');
|
|
329
|
+
});
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
describe('array key handling issue #574', function () {
|
|
333
|
+
|
|
334
|
+
// net: array indices are reported as string path segments. getKeys() has a
|
|
335
|
+
// dead numeric-index branch; replacing it would turn ["0"] into [0] and
|
|
336
|
+
// change every array diff path, so these pin the contract first.
|
|
337
|
+
it('should report deletions with string index paths when an array shrinks', function () {
|
|
338
|
+
let d = diff({a: [1, 2, 3]}, {a: [1]});
|
|
339
|
+
|
|
340
|
+
expect(JSON.stringify(d)).is.equal('[{"operator":"delete","path":["a","1"],"first":{"value":2,"type":"number"}},{"operator":"delete","path":["a","2"],"first":{"value":3,"type":"number"}}]');
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('should report additions with string index paths when an array grows', function () {
|
|
344
|
+
let d = diff({a: [1]}, {a: [1, 2]});
|
|
345
|
+
|
|
346
|
+
expect(JSON.stringify(d)).is.equal('[{"operator":"add","path":["a","1"],"second":{"value":2,"type":"number"}}]');
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('should report an update with a string index path when an element changes', function () {
|
|
350
|
+
let d = diff({a: [1, 2]}, {a: [1, 9]});
|
|
351
|
+
|
|
352
|
+
expect(JSON.stringify(d)).is.equal('[{"operator":"update","path":["a","1"],"first":{"value":2,"type":"number"},"second":{"value":9,"type":"number"}}]');
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('should report no change for identical arrays', function () {
|
|
356
|
+
expect(JSON.stringify(diff({a: [1, 2, 3]}, {a: [1, 2, 3]}))).is.equal('[]');
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('should diff nested arrays of objects with string index paths', function () {
|
|
360
|
+
let d = diff({rows: [{id: 1}, {id: 2}]}, {rows: [{id: 1}, {id: 3}]});
|
|
361
|
+
|
|
362
|
+
expect(JSON.stringify(d)).is.equal('[{"operator":"update","path":["rows","1","id"],"first":{"value":2,"type":"number"},"second":{"value":3,"type":"number"}}]');
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('should treat a sparse array hole as absent rather than as undefined', function () {
|
|
366
|
+
let sparse = [1];
|
|
367
|
+
sparse[2] = 3;
|
|
368
|
+
|
|
369
|
+
expect(JSON.stringify(diff({a: sparse}, {a: sparse}))).is.equal('[]');
|
|
370
|
+
expect(Object.hasOwn(sparse, '1')).to.be.false;
|
|
371
|
+
});
|
|
372
|
+
})
|
|
373
|
+
|
|
288
374
|
})
|
|
@@ -183,6 +183,58 @@ describe('extend function', () => {
|
|
|
183
183
|
}
|
|
184
184
|
});
|
|
185
185
|
});
|
|
186
|
+
|
|
187
|
+
// net: behaviour that is already correct and must stay correct
|
|
188
|
+
it('should keep merging nested objects without losing existing keys issue #573', () => {
|
|
189
|
+
const target = { a: { b: 1, keep: 'yes' } };
|
|
190
|
+
const source = { a: { c: 2 } };
|
|
191
|
+
|
|
192
|
+
expect(extend(target, source)).to.deep.equal({ a: { b: 1, c: 2, keep: 'yes' } });
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should keep throwing on a type mismatch issue #573', () => {
|
|
196
|
+
expect(() => extend({ a: { b: 1 } }, { a: [1, 2] })).to.throw();
|
|
197
|
+
expect(() => extend({ a: [1, 2] }, { a: { b: 1 } })).to.throw();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should keep throwing for unsupported arguments issue #573', () => {
|
|
201
|
+
expect(() => extend({ a: 1 }, 'not an object')).to.throw();
|
|
202
|
+
expect(() => extend(null, { a: 1 })).to.throw();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// regression: a cyclic source must terminate instead of exhausting the stack
|
|
206
|
+
it('should extend from a self referencing source issue #573', () => {
|
|
207
|
+
const source = { name: 'root' };
|
|
208
|
+
source.self = source;
|
|
209
|
+
|
|
210
|
+
const result = extend({}, source);
|
|
211
|
+
|
|
212
|
+
expect(result.name).to.equal('root');
|
|
213
|
+
expect(result.self).to.be.an('object');
|
|
214
|
+
expect(result.self.name).to.equal('root');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should extend from a mutual reference between two sources issue #573', () => {
|
|
218
|
+
const first = { name: 'first' };
|
|
219
|
+
const second = { name: 'second', first };
|
|
220
|
+
first.second = second;
|
|
221
|
+
|
|
222
|
+
const result = extend({}, first);
|
|
223
|
+
|
|
224
|
+
expect(result.name).to.equal('first');
|
|
225
|
+
expect(result.second.name).to.equal('second');
|
|
226
|
+
expect(result.second.first).to.be.an('object');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('should extend from a cycle that runs through an array issue #573', () => {
|
|
230
|
+
const source = { items: [] };
|
|
231
|
+
source.items.push(source);
|
|
232
|
+
|
|
233
|
+
const result = extend({}, source);
|
|
234
|
+
|
|
235
|
+
expect(result.items).to.be.an('array');
|
|
236
|
+
expect(result.items.length).to.equal(1);
|
|
237
|
+
});
|
|
186
238
|
});
|
|
187
239
|
|
|
188
240
|
|