@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.
- 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 +6 -5
- package/source/data/buildtree.mjs +0 -8
- 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 +228 -32
- package/source/data/transformer.mjs +2 -2
- package/source/dom/customcontrol.mjs +35 -9
- package/source/dom/customelement.mjs +293 -31
- package/source/dom/slotted.mjs +46 -31
- package/source/dom/template.mjs +4 -1
- package/source/dom/updater.mjs +57 -17
- package/source/types/base.mjs +7 -6
- package/source/types/basewithoptions.mjs +1 -8
- package/source/types/internal.mjs +4 -10
- package/source/types/node.mjs +1 -3
- package/source/types/observerlist.mjs +9 -17
- package/source/types/proxyobserver.mjs +51 -12
- 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/buildmap.mjs +14 -0
- package/test/cases/data/buildtree.mjs +49 -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 +163 -0
- package/test/cases/data/transformer.mjs +26 -0
- package/test/cases/dom/customcontrol.mjs +85 -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 +408 -0
- package/test/cases/dom/dimension.mjs +34 -10
- package/test/cases/dom/slotted-nodes.mjs +48 -1
- package/test/cases/dom/template.mjs +11 -0
- package/test/cases/dom/updater-remove.mjs +198 -0
- package/test/cases/dom/updater.mjs +118 -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 +189 -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
|
@@ -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);
|
|
@@ -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
|
/**
|
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
|
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import {expect} from "chai"
|
|
4
|
+
import {Pathfinder} from "../../../source/data/pathfinder.mjs";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Baseline for issue #576.
|
|
8
|
+
*
|
|
9
|
+
* getOption() is called about 1393 times across the library and resolves every
|
|
10
|
+
* option through Pathfinder, today by throwing and catching. Before that read
|
|
11
|
+
* path is replaced, this file freezes the resolution semantics that callers
|
|
12
|
+
* depend on. Every assertion here passes with the old implementation and must
|
|
13
|
+
* keep passing with the new one.
|
|
14
|
+
*/
|
|
15
|
+
describe('Pathfinder resolution baseline issue #576', function () {
|
|
16
|
+
|
|
17
|
+
class WithPrototypeMember {
|
|
18
|
+
constructor() {
|
|
19
|
+
this.own = 1;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
inherited() {
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function buildSubject() {
|
|
27
|
+
const nullProto = Object.create(null);
|
|
28
|
+
nullProto.leaf = 'from-null-proto';
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
flat: 'value',
|
|
32
|
+
zero: 0,
|
|
33
|
+
emptyString: '',
|
|
34
|
+
falseValue: false,
|
|
35
|
+
nullValue: null,
|
|
36
|
+
undefinedValue: undefined,
|
|
37
|
+
nested: {
|
|
38
|
+
deep: {
|
|
39
|
+
leaf: 'deep-leaf',
|
|
40
|
+
},
|
|
41
|
+
list: ['first', 'second', 'third'],
|
|
42
|
+
objects: [{id: 1}, {id: 2}],
|
|
43
|
+
},
|
|
44
|
+
map: new Map([['key', 'map-value'], ['nested', {leaf: 'map-leaf'}]]),
|
|
45
|
+
set: new Set(['set-first', 'set-second']),
|
|
46
|
+
instance: new WithPrototypeMember(),
|
|
47
|
+
nullProto,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('getVia() resolved values', function () {
|
|
52
|
+
|
|
53
|
+
[
|
|
54
|
+
['flat', 'value'],
|
|
55
|
+
['zero', 0],
|
|
56
|
+
['emptyString', ''],
|
|
57
|
+
['falseValue', false],
|
|
58
|
+
['nullValue', null],
|
|
59
|
+
['undefinedValue', undefined],
|
|
60
|
+
['nested.deep.leaf', 'deep-leaf'],
|
|
61
|
+
['nested.list.0', 'first'],
|
|
62
|
+
['nested.list.2', 'third'],
|
|
63
|
+
['nested.objects.1.id', 2],
|
|
64
|
+
['map.key', 'map-value'],
|
|
65
|
+
['map.nested.leaf', 'map-leaf'],
|
|
66
|
+
['set.0', 'set-first'],
|
|
67
|
+
['set.1', 'set-second'],
|
|
68
|
+
['instance.own', 1],
|
|
69
|
+
['nullProto.leaf', 'from-null-proto'],
|
|
70
|
+
].forEach(function (data) {
|
|
71
|
+
const path = data[0];
|
|
72
|
+
const expected = data[1];
|
|
73
|
+
|
|
74
|
+
it(`getVia(${path}) should resolve to ${JSON.stringify(expected)}`, function () {
|
|
75
|
+
expect(new Pathfinder(buildSubject()).getVia(path)).to.equal(expected);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('getVia() should resolve a prototype member', function () {
|
|
80
|
+
const value = new Pathfinder(buildSubject()).getVia('instance.inherited');
|
|
81
|
+
expect(typeof value).to.equal('function');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('getVia() should return the root for an empty path', function () {
|
|
85
|
+
const subject = buildSubject();
|
|
86
|
+
expect(new Pathfinder(subject).getVia('')).to.equal(subject);
|
|
87
|
+
expect(new Pathfinder(subject).getVia([])).to.equal(subject);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('getVia() should accept an array path and not mutate it', function () {
|
|
91
|
+
const path = ['nested', 'deep', 'leaf'];
|
|
92
|
+
expect(new Pathfinder(buildSubject()).getVia(path)).to.equal('deep-leaf');
|
|
93
|
+
expect(path).to.deep.equal(['nested', 'deep', 'leaf']);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('getVia() missing leaves resolve to undefined', function () {
|
|
98
|
+
|
|
99
|
+
[
|
|
100
|
+
'missing',
|
|
101
|
+
'nested.missing',
|
|
102
|
+
'nested.deep.missing',
|
|
103
|
+
'map.missing',
|
|
104
|
+
].forEach(function (path) {
|
|
105
|
+
it(`getVia(${path}) should resolve to undefined`, function () {
|
|
106
|
+
expect(new Pathfinder(buildSubject()).getVia(path)).to.equal(undefined);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('getVia() throws past a non traversable value', function () {
|
|
112
|
+
|
|
113
|
+
[
|
|
114
|
+
'flat.deeper',
|
|
115
|
+
'zero.deeper',
|
|
116
|
+
'nested.deep.leaf.deeper',
|
|
117
|
+
'undefinedValue.deeper',
|
|
118
|
+
'nullValue.deeper',
|
|
119
|
+
'nested.list.0.deeper',
|
|
120
|
+
].forEach(function (path) {
|
|
121
|
+
it(`getVia(${path}) should throw`, function () {
|
|
122
|
+
expect(() => new Pathfinder(buildSubject()).getVia(path)).to.throw();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should report that the journey is not at its end', function () {
|
|
127
|
+
expect(() => new Pathfinder(buildSubject()).getVia('flat.deeper'))
|
|
128
|
+
.to.throw(Error, 'the journey is not at its end');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should reject an index with trailing characters', function () {
|
|
132
|
+
expect(() => new Pathfinder(buildSubject()).getVia('nested.list.1suffix'))
|
|
133
|
+
.to.throw(TypeError, 'value is not an integer');
|
|
134
|
+
expect(() => new Pathfinder(buildSubject()).getVia('set.1suffix'))
|
|
135
|
+
.to.throw(TypeError, 'value is not an integer');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should reject unsupported weak collections', function () {
|
|
139
|
+
expect(() => new Pathfinder({weak: new WeakSet()}).getVia('weak.entry'))
|
|
140
|
+
.to.throw(Error, 'unsupported action for this data type (WeakSet)');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should reject a non string and non array path', function () {
|
|
144
|
+
expect(() => new Pathfinder(buildSubject()).getVia(42)).to.throw(Error);
|
|
145
|
+
expect(() => new Pathfinder(buildSubject()).getVia(undefined)).to.throw(Error);
|
|
146
|
+
expect(() => new Pathfinder(buildSubject()).getVia(null)).to.throw(Error);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('exists() membership semantics', function () {
|
|
151
|
+
|
|
152
|
+
[
|
|
153
|
+
['flat', true],
|
|
154
|
+
['zero', true],
|
|
155
|
+
['emptyString', true],
|
|
156
|
+
['falseValue', true],
|
|
157
|
+
['nullValue', true],
|
|
158
|
+
['undefinedValue', true],
|
|
159
|
+
['missing', false],
|
|
160
|
+
['nested.deep.leaf', true],
|
|
161
|
+
['nested.deep.missing', false],
|
|
162
|
+
['nested.missing.leaf', false],
|
|
163
|
+
['nested.list.0', true],
|
|
164
|
+
['nested.list.2', true],
|
|
165
|
+
['nested.list.3', false],
|
|
166
|
+
['nested.objects.1.id', true],
|
|
167
|
+
['map.key', true],
|
|
168
|
+
['map.missing', false],
|
|
169
|
+
['set.0', true],
|
|
170
|
+
['set.1', true],
|
|
171
|
+
['set.2', false],
|
|
172
|
+
['instance.own', true],
|
|
173
|
+
['instance.inherited', true],
|
|
174
|
+
['nullProto.leaf', true],
|
|
175
|
+
['nullProto.missing', false],
|
|
176
|
+
['flat.deeper', false],
|
|
177
|
+
].forEach(function (data) {
|
|
178
|
+
const path = data[0];
|
|
179
|
+
const expected = data[1];
|
|
180
|
+
|
|
181
|
+
it(`exists(${path}) should be ${expected}`, function () {
|
|
182
|
+
expect(new Pathfinder(buildSubject()).exists(path)).to.equal(expected);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('exists() should report a present key whose value is undefined', function () {
|
|
187
|
+
expect(new Pathfinder({a: {b: undefined}}).exists('a.b')).to.equal(true);
|
|
188
|
+
expect(new Pathfinder({a: {b: undefined}}).exists('a.c')).to.equal(false);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// load bearing: init-options-from-attributes.mjs and
|
|
192
|
+
// set-option-from-attribute.mjs call exists() with an undefined option
|
|
193
|
+
// name for every unknown data-monster-option-* attribute
|
|
194
|
+
it('exists() should return false instead of throwing for a malformed path', function () {
|
|
195
|
+
const pathfinder = new Pathfinder(buildSubject());
|
|
196
|
+
|
|
197
|
+
expect(pathfinder.exists(undefined)).to.equal(false);
|
|
198
|
+
expect(pathfinder.exists(null)).to.equal(false);
|
|
199
|
+
expect(pathfinder.exists(42)).to.equal(false);
|
|
200
|
+
expect(pathfinder.exists({})).to.equal(false);
|
|
201
|
+
expect(pathfinder.exists('nested.list.1suffix')).to.equal(false);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('exists() should accept an array path', function () {
|
|
205
|
+
expect(new Pathfinder(buildSubject()).exists(['nested', 'deep', 'leaf'])).to.equal(true);
|
|
206
|
+
expect(new Pathfinder(buildSubject()).exists(['nested', 'deep', 'nope'])).to.equal(false);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe('setVia() vivification', function () {
|
|
211
|
+
|
|
212
|
+
it('should build nested objects', function () {
|
|
213
|
+
const subject = {};
|
|
214
|
+
new Pathfinder(subject).setVia('a.b.c', true);
|
|
215
|
+
expect(JSON.stringify(subject)).to.equal('{"a":{"b":{"c":true}}}');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should build a sparse array for an integer segment', function () {
|
|
219
|
+
const subject = {};
|
|
220
|
+
new Pathfinder(subject).setVia('a.b.3.c', true);
|
|
221
|
+
expect(JSON.stringify(subject)).to.equal('{"a":{"b":[null,null,null,{"c":true}]}}');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('should build nested arrays', function () {
|
|
225
|
+
const subject = {};
|
|
226
|
+
new Pathfinder(subject).setVia('a.b.0.0.c', true);
|
|
227
|
+
expect(JSON.stringify(subject)).to.equal('{"a":{"b":[[{"c":true}]]}}');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should add to a Set and ignore the key', function () {
|
|
231
|
+
const values = new Set(['first']);
|
|
232
|
+
new Pathfinder({values}).setVia('values.anything', 'second');
|
|
233
|
+
expect([...values]).to.deep.equal(['first', 'second']);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should write into a Map', function () {
|
|
237
|
+
const map = new Map();
|
|
238
|
+
new Pathfinder({map}).setVia('map.key', 'written');
|
|
239
|
+
expect(map.get('key')).to.equal('written');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('should reject a WeakSet write explicitly', function () {
|
|
243
|
+
expect(() => new Pathfinder({values: new WeakSet()}).setVia('values.next', {}))
|
|
244
|
+
.to.throw(Error, 'unsupported action for this data type in setValueViaPath');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('should reject unsafe mutation path segments', function () {
|
|
248
|
+
expect(() => new Pathfinder({}).setVia('__proto__.polluted', true)).to.throw(Error);
|
|
249
|
+
expect(() => new Pathfinder({}).setVia('constructor.prototype.polluted', true)).to.throw(Error);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe('wildcard resolution', function () {
|
|
254
|
+
|
|
255
|
+
function wildcardSubject() {
|
|
256
|
+
return {
|
|
257
|
+
a: {
|
|
258
|
+
b: {leaf: 1},
|
|
259
|
+
c: {leaf: 2},
|
|
260
|
+
},
|
|
261
|
+
rows: [{value: 'x'}, {value: 'y'}],
|
|
262
|
+
map: new Map([['first', {leaf: 'm1'}], ['second', {leaf: 'm2'}]]),
|
|
263
|
+
set: new Set(['s1', 's2']),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
it('should return a Map for a trailing wildcard', function () {
|
|
268
|
+
const result = new Pathfinder(wildcardSubject()).getVia('a.*');
|
|
269
|
+
expect(result).to.be.instanceOf(Map);
|
|
270
|
+
expect([...result.keys()]).to.deep.equal(['b', 'c']);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('should resolve a path behind a wildcard', function () {
|
|
274
|
+
const result = new Pathfinder(wildcardSubject()).getVia('a.*.leaf');
|
|
275
|
+
expect(Array.from(result)).to.deep.equal([['b', 1], ['c', 2]]);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('should enumerate array entries behind a wildcard', function () {
|
|
279
|
+
const result = new Pathfinder(wildcardSubject()).getVia('rows.*.value');
|
|
280
|
+
expect(Array.from(result)).to.deep.equal([['0', 'x'], ['1', 'y']]);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('should enumerate Map values behind a wildcard', function () {
|
|
284
|
+
const result = new Pathfinder(wildcardSubject()).getVia('map.*.leaf');
|
|
285
|
+
expect(Array.from(result)).to.deep.equal([['first', 'm1'], ['second', 'm2']]);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('should enumerate Set values behind a wildcard with stringified indices', function () {
|
|
289
|
+
const result = new Pathfinder(wildcardSubject()).getVia('set.*');
|
|
290
|
+
expect(Array.from(result)).to.deep.equal([['0', 's1'], ['1', 's2']]);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should support a leading wildcard', function () {
|
|
294
|
+
const result = new Pathfinder({a: {x: 1}, b: {x: 2}}).getVia('*.x');
|
|
295
|
+
expect(Array.from(result)).to.deep.equal([['a', 1], ['b', 2]]);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('exists() should work through a wildcard', function () {
|
|
299
|
+
expect(new Pathfinder(wildcardSubject()).exists('a.*.leaf')).to.equal(true);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
describe('deleteVia()', function () {
|
|
304
|
+
|
|
305
|
+
it('should delete an object key', function () {
|
|
306
|
+
const subject = {a: {b: 1, c: 2}};
|
|
307
|
+
new Pathfinder(subject).deleteVia('a.b');
|
|
308
|
+
expect(subject).to.deep.equal({a: {c: 2}});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('should delete a Map entry', function () {
|
|
312
|
+
const map = new Map([['key', 1]]);
|
|
313
|
+
new Pathfinder({map}).deleteVia('map.key');
|
|
314
|
+
expect(map.has('key')).to.equal(false);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('should reject deleting from a Set', function () {
|
|
318
|
+
expect(() => new Pathfinder({values: new Set(['a'])}).deleteVia('values.0'))
|
|
319
|
+
.to.throw(Error, 'unsupported action for this data type in deleteValueViaPath');
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
});
|