@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
|
@@ -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) {
|
|
@@ -523,4 +557,133 @@ describe('Pathfinder', function () {
|
|
|
523
557
|
});
|
|
524
558
|
|
|
525
559
|
});
|
|
560
|
+
|
|
561
|
+
describe('undefined writes keep the key issue #574', function () {
|
|
562
|
+
|
|
563
|
+
// net: setVia() is a write, deleteVia() is the deletion API. Writing
|
|
564
|
+
// undefined must not delete, otherwise a default option declared as
|
|
565
|
+
// undefined would vanish and set-option-from-attribute would stop
|
|
566
|
+
// accepting later attribute changes for it.
|
|
567
|
+
it('setVia() should keep an existing object key when writing undefined', function () {
|
|
568
|
+
const subject = {a: {b: 1}};
|
|
569
|
+
|
|
570
|
+
new Pathfinder(subject).setVia('a.b', undefined);
|
|
571
|
+
|
|
572
|
+
expect(Object.hasOwn(subject.a, 'b')).to.be.true;
|
|
573
|
+
expect(subject.a.b).to.equal(undefined);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it('setVia() should create a key when writing undefined to a new path', function () {
|
|
577
|
+
const subject = {};
|
|
578
|
+
|
|
579
|
+
new Pathfinder(subject).setVia('a.b', undefined);
|
|
580
|
+
|
|
581
|
+
expect(Object.hasOwn(subject.a, 'b')).to.be.true;
|
|
582
|
+
expect(subject.a.b).to.equal(undefined);
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
it('exists() should report a key written as undefined', function () {
|
|
586
|
+
const subject = {a: {b: 1}};
|
|
587
|
+
const pathfinder = new Pathfinder(subject);
|
|
588
|
+
|
|
589
|
+
pathfinder.setVia('a.b', undefined);
|
|
590
|
+
|
|
591
|
+
expect(pathfinder.exists('a.b')).to.be.true;
|
|
592
|
+
expect(pathfinder.exists('a.c')).to.be.false;
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
it('deleteVia() should remove a key that was written as undefined', function () {
|
|
596
|
+
const subject = {a: {b: 1, c: 2}};
|
|
597
|
+
const pathfinder = new Pathfinder(subject);
|
|
598
|
+
|
|
599
|
+
pathfinder.setVia('a.b', undefined);
|
|
600
|
+
pathfinder.deleteVia('a.b');
|
|
601
|
+
|
|
602
|
+
expect(Object.hasOwn(subject.a, 'b')).to.be.false;
|
|
603
|
+
expect(subject.a).to.deep.equal({c: 2});
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it('setVia() should keep an array slot when writing undefined', function () {
|
|
607
|
+
const subject = {a: [1, 2, 3]};
|
|
608
|
+
|
|
609
|
+
new Pathfinder(subject).setVia('a.1', undefined);
|
|
610
|
+
|
|
611
|
+
expect(subject.a.length).to.equal(3);
|
|
612
|
+
expect(Object.hasOwn(subject.a, '1')).to.be.true;
|
|
613
|
+
expect(subject.a[1]).to.equal(undefined);
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
it('deleteVia() should clear an array slot', function () {
|
|
617
|
+
const subject = {a: [1, 2, 3]};
|
|
618
|
+
|
|
619
|
+
new Pathfinder(subject).deleteVia('a.1');
|
|
620
|
+
|
|
621
|
+
expect(subject.a.length).to.equal(3);
|
|
622
|
+
expect(Object.hasOwn(subject.a, '1')).to.be.false;
|
|
623
|
+
});
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
describe('getVia default value issue #576', function () {
|
|
627
|
+
|
|
628
|
+
function subject() {
|
|
629
|
+
return {
|
|
630
|
+
data: {
|
|
631
|
+
calculated: false,
|
|
632
|
+
space: 0,
|
|
633
|
+
label: '',
|
|
634
|
+
button: {},
|
|
635
|
+
list: [1, 2],
|
|
636
|
+
},
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// net: without a second argument the throwing contract is unchanged
|
|
641
|
+
it('should keep throwing past a non traversable value when no default is given', function () {
|
|
642
|
+
expect(() => new Pathfinder(subject()).getVia('data.label.deeper'))
|
|
643
|
+
.to.throw(Error, 'the journey is not at its end');
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
it('should keep resolving a missing leaf to undefined when no default is given', function () {
|
|
647
|
+
expect(new Pathfinder(subject()).getVia('data.missing')).to.equal(undefined);
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
// regression: the second argument was accepted and silently ignored
|
|
651
|
+
it('should return the default instead of throwing past a non traversable value', function () {
|
|
652
|
+
expect(new Pathfinder(subject()).getVia('data.label.deeper', 'fallback')).to.equal('fallback');
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
it('should return the default for a missing mid path segment', function () {
|
|
656
|
+
expect(new Pathfinder(subject()).getVia('data.button.unseeded.width', 0)).to.equal(0);
|
|
657
|
+
expect(new Pathfinder(subject()).getVia('missing.deep.path', 'fallback')).to.equal('fallback');
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
it('should return the default for a missing leaf', function () {
|
|
661
|
+
expect(new Pathfinder(subject()).getVia('data.button.unseeded', 0)).to.equal(0);
|
|
662
|
+
expect(new Pathfinder(subject()).getVia('data.missing', 'fallback')).to.equal('fallback');
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
it('should return the default for a malformed path instead of throwing', function () {
|
|
666
|
+
expect(new Pathfinder(subject()).getVia(undefined, 'fallback')).to.equal('fallback');
|
|
667
|
+
expect(new Pathfinder(subject()).getVia('data.list.1suffix', 'fallback')).to.equal('fallback');
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
it('should return the resolved value and not the default when the path exists', function () {
|
|
671
|
+
expect(new Pathfinder(subject()).getVia('data.calculated', true)).to.equal(false);
|
|
672
|
+
expect(new Pathfinder(subject()).getVia('data.space', 99)).to.equal(0);
|
|
673
|
+
expect(new Pathfinder(subject()).getVia('data.label', 'fallback')).to.equal('');
|
|
674
|
+
expect(new Pathfinder(subject()).getVia('data.list.0', 99)).to.equal(1);
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
it('should return the default for a key whose value is undefined', function () {
|
|
678
|
+
expect(new Pathfinder({a: {b: undefined}}).getVia('a.b', 'fallback')).to.equal('fallback');
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
it('should return the default for a null prototype subject without a match', function () {
|
|
682
|
+
const nullProto = Object.create(null);
|
|
683
|
+
nullProto.leaf = 1;
|
|
684
|
+
|
|
685
|
+
expect(new Pathfinder({nullProto}).getVia('nullProto.leaf', 9)).to.equal(1);
|
|
686
|
+
expect(new Pathfinder({nullProto}).getVia('nullProto.missing', 9)).to.equal(9);
|
|
687
|
+
});
|
|
688
|
+
});
|
|
526
689
|
});
|
|
@@ -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);
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import * as chai from "chai";
|
|
4
|
+
import { customElementUpdaterLinkSymbol } from "../../../source/dom/constants.mjs";
|
|
5
|
+
import { getDocument } from "../../../source/dom/util.mjs";
|
|
6
|
+
import { chaiDom } from "../../util/chai-dom.mjs";
|
|
7
|
+
import { initJSDOM } from "../../util/jsdom.mjs";
|
|
8
|
+
|
|
9
|
+
let expect = chai.expect;
|
|
10
|
+
chai.use(chaiDom);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Baseline for issue #575.
|
|
14
|
+
*
|
|
15
|
+
* Before the base class starts disposing updaters on disconnect and rebinding
|
|
16
|
+
* on reconnect, this file pins the updater contract on a live element. Every
|
|
17
|
+
* assertion passes today, and the whole contract is checked twice: once on a
|
|
18
|
+
* freshly inserted element, and once after the element has been removed and
|
|
19
|
+
* appended again. The second pass is what proves a rebind actually restores
|
|
20
|
+
* rendering, event processing and pipe callbacks.
|
|
21
|
+
*/
|
|
22
|
+
describe("CustomElement rebind baseline issue #575", function () {
|
|
23
|
+
let CustomElement;
|
|
24
|
+
let registerCustomElement;
|
|
25
|
+
let updaterTransformerMethodsSymbol;
|
|
26
|
+
let document;
|
|
27
|
+
|
|
28
|
+
const TAG = "monster-rebind-baseline-575";
|
|
29
|
+
const CHILD_TAG = "monster-rebind-baseline-child-575";
|
|
30
|
+
|
|
31
|
+
const html = `
|
|
32
|
+
<div id="test1"></div>
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
before(function (done) {
|
|
36
|
+
initJSDOM({})
|
|
37
|
+
.then(() => import("../../../source/dom/customelement.mjs"))
|
|
38
|
+
.then((m) => {
|
|
39
|
+
try {
|
|
40
|
+
CustomElement = m["CustomElement"];
|
|
41
|
+
registerCustomElement = m["registerCustomElement"];
|
|
42
|
+
updaterTransformerMethodsSymbol =
|
|
43
|
+
m["updaterTransformerMethodsSymbol"];
|
|
44
|
+
document = getDocument();
|
|
45
|
+
|
|
46
|
+
if (!customElements.get(CHILD_TAG)) {
|
|
47
|
+
class RebindBaselineChild extends CustomElement {
|
|
48
|
+
static getTag() {
|
|
49
|
+
return CHILD_TAG;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get defaults() {
|
|
53
|
+
return Object.assign({}, super.defaults, {
|
|
54
|
+
label: "child-initial",
|
|
55
|
+
templates: {
|
|
56
|
+
main: '<span id="child-label" data-monster-replace="path:label"></span>',
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
registerCustomElement(RebindBaselineChild);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!customElements.get(TAG)) {
|
|
66
|
+
class RebindBaseline extends CustomElement {
|
|
67
|
+
static getTag() {
|
|
68
|
+
return TAG;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get defaults() {
|
|
72
|
+
return Object.assign({}, super.defaults, {
|
|
73
|
+
eventProcessing: true,
|
|
74
|
+
headline: "start",
|
|
75
|
+
shout: "quiet",
|
|
76
|
+
cssClass: "alpha",
|
|
77
|
+
inputValue: "typed",
|
|
78
|
+
checkedFlag: false,
|
|
79
|
+
patchText: "patch-start",
|
|
80
|
+
keyed: [
|
|
81
|
+
{ id: "k1", label: "first" },
|
|
82
|
+
{ id: "k2", label: "second" },
|
|
83
|
+
],
|
|
84
|
+
items: [{ name: "item-a" }, { name: "item-b" }],
|
|
85
|
+
templates: {
|
|
86
|
+
main: `
|
|
87
|
+
<template id="rebind-item-575">
|
|
88
|
+
<li data-monster-replace="path:rebind-item-575.name"></li>
|
|
89
|
+
</template>
|
|
90
|
+
<h1 id="headline" data-monster-replace="path:headline"></h1>
|
|
91
|
+
<span id="shouted" data-monster-replace="path:shout | call:shout"></span>
|
|
92
|
+
<div id="classed" data-monster-attributes="class path:cssClass"></div>
|
|
93
|
+
<input id="bound" type="text" data-monster-bind="path:inputValue" data-monster-attributes="value path:inputValue">
|
|
94
|
+
<input id="flag" type="checkbox" data-monster-properties="checked path:checkedFlag">
|
|
95
|
+
<div id="patched" data-monster-patch="path:patchText"></div>
|
|
96
|
+
<div id="keyed" data-monster-patch="path:keyed" data-monster-patch-key="path:id" data-monster-patch-render="path:label"></div>
|
|
97
|
+
<ul id="inserted" data-monster-insert="rebind-item-575 path:items"></ul>
|
|
98
|
+
<select id="native-select"><option>one</option></select>
|
|
99
|
+
<${CHILD_TAG} id="child"></${CHILD_TAG}>
|
|
100
|
+
`,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
[updaterTransformerMethodsSymbol]() {
|
|
106
|
+
return {
|
|
107
|
+
shout: (value) => String(value).toUpperCase(),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
registerCustomElement(RebindBaseline);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
done();
|
|
116
|
+
} catch (e) {
|
|
117
|
+
done(e);
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
.catch(done);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
let element;
|
|
124
|
+
|
|
125
|
+
beforeEach(async function () {
|
|
126
|
+
const mocks = document.getElementById("mocks");
|
|
127
|
+
mocks.innerHTML = html;
|
|
128
|
+
|
|
129
|
+
element = document.createElement(TAG);
|
|
130
|
+
document.getElementById("test1").appendChild(element);
|
|
131
|
+
await settle();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
afterEach(function () {
|
|
135
|
+
const mocks = document.getElementById("mocks");
|
|
136
|
+
mocks.innerHTML = "";
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
function settle(ms = 30) {
|
|
140
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function query(id) {
|
|
144
|
+
return element.shadowRoot.querySelector(`#${id}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function reconnect() {
|
|
148
|
+
const parent = element.parentNode;
|
|
149
|
+
element.remove();
|
|
150
|
+
await settle();
|
|
151
|
+
parent.appendChild(element);
|
|
152
|
+
await settle();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The whole updater contract. Called once on a fresh element and once after a
|
|
157
|
+
* reconnect, so both runs assert exactly the same behaviour.
|
|
158
|
+
*/
|
|
159
|
+
async function assertUpdaterContract(label) {
|
|
160
|
+
const because = ` (${label})`;
|
|
161
|
+
|
|
162
|
+
expect(element.hasAttribute("data-monster-error"), `no error${because}`).to
|
|
163
|
+
.be.false;
|
|
164
|
+
|
|
165
|
+
// data-monster-replace
|
|
166
|
+
element.setOption("headline", `headline-${label}`);
|
|
167
|
+
await settle();
|
|
168
|
+
expect(query("headline").textContent, `replace${because}`).to.equal(
|
|
169
|
+
`headline-${label}`,
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// pipe callback from updaterTransformerMethodsSymbol
|
|
173
|
+
element.setOption("shout", `shout-${label}`);
|
|
174
|
+
await settle();
|
|
175
|
+
expect(query("shouted").textContent, `pipe${because}`).to.equal(
|
|
176
|
+
`shout-${label}`.toUpperCase(),
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// data-monster-attributes
|
|
180
|
+
element.setOption("cssClass", `class-${label}`);
|
|
181
|
+
await settle();
|
|
182
|
+
expect(query("classed").getAttribute("class"), `attributes${because}`).to.
|
|
183
|
+
equal(`class-${label}`);
|
|
184
|
+
|
|
185
|
+
// data-monster-properties
|
|
186
|
+
element.setOption("checkedFlag", true);
|
|
187
|
+
await settle();
|
|
188
|
+
expect(query("flag").checked, `properties on${because}`).to.equal(true);
|
|
189
|
+
element.setOption("checkedFlag", false);
|
|
190
|
+
await settle();
|
|
191
|
+
expect(query("flag").checked, `properties off${because}`).to.equal(false);
|
|
192
|
+
|
|
193
|
+
// data-monster-patch
|
|
194
|
+
element.setOption("patchText", `patch-${label}`);
|
|
195
|
+
await settle();
|
|
196
|
+
expect(query("patched").textContent, `patch${because}`).to.equal(
|
|
197
|
+
`patch-${label}`,
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
// keyed patch renders one node per key and keeps node identity for a key
|
|
201
|
+
// whose data did not change
|
|
202
|
+
const keyedNodes = [...query("keyed").childNodes];
|
|
203
|
+
expect(keyedNodes.length, `keyed count${because}`).to.equal(2);
|
|
204
|
+
const firstKeyedNode = keyedNodes[0];
|
|
205
|
+
const firstLabel = element.getOption("keyed.0.label");
|
|
206
|
+
|
|
207
|
+
element.setOption("keyed.1.label", `second-${label}`);
|
|
208
|
+
await settle();
|
|
209
|
+
|
|
210
|
+
expect(
|
|
211
|
+
[...query("keyed").childNodes][0],
|
|
212
|
+
`keyed identity${because}`,
|
|
213
|
+
).to.equal(firstKeyedNode);
|
|
214
|
+
expect(
|
|
215
|
+
[...query("keyed").childNodes].map((node) => node.textContent),
|
|
216
|
+
`keyed render${because}`,
|
|
217
|
+
).to.deep.equal([firstLabel, `second-${label}`]);
|
|
218
|
+
|
|
219
|
+
// data-monster-insert
|
|
220
|
+
expect(query("inserted").children.length, `insert${because}`).to.equal(2);
|
|
221
|
+
element.setOption("items.0.name", `item-${label}`);
|
|
222
|
+
await settle();
|
|
223
|
+
expect(query("inserted").textContent, `insert update${because}`).to.contain(
|
|
224
|
+
`item-${label}`,
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
// nested component renders its own options
|
|
228
|
+
const child = query("child");
|
|
229
|
+
expect(child, `child present${because}`).to.not.equal(null);
|
|
230
|
+
child.setOption("label", `child-${label}`);
|
|
231
|
+
await settle();
|
|
232
|
+
expect(
|
|
233
|
+
child.shadowRoot.querySelector("#child-label").textContent,
|
|
234
|
+
`child update${because}`,
|
|
235
|
+
).to.equal(`child-${label}`);
|
|
236
|
+
|
|
237
|
+
// event processing writes a typed value into the updater subject, which is
|
|
238
|
+
// the options snapshot, not the component options. The one way sync means
|
|
239
|
+
// getOption() never sees user input, so the snapshot is the only place the
|
|
240
|
+
// value lives and a rebind must not discard it.
|
|
241
|
+
const bound = query("bound");
|
|
242
|
+
bound.value = `typed-${label}`;
|
|
243
|
+
bound.dispatchEvent(new Event("input", { bubbles: true, composed: true }));
|
|
244
|
+
await settle(150);
|
|
245
|
+
expect(
|
|
246
|
+
element.getInternalUpdateCloneData()?.inputValue,
|
|
247
|
+
`bind writeback${because}`,
|
|
248
|
+
).to.equal(`typed-${label}`);
|
|
249
|
+
|
|
250
|
+
// disabled propagation reaches native controls inside the shadow root
|
|
251
|
+
element.setOption("disabled", true);
|
|
252
|
+
await settle();
|
|
253
|
+
expect(
|
|
254
|
+
query("native-select").hasAttribute("disabled"),
|
|
255
|
+
`disabled${because}`,
|
|
256
|
+
).to.be.true;
|
|
257
|
+
element.setOption("disabled", false);
|
|
258
|
+
await settle();
|
|
259
|
+
expect(
|
|
260
|
+
query("native-select").hasAttribute("disabled"),
|
|
261
|
+
`disabled off${because}`,
|
|
262
|
+
).to.be.false;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
it("should honor the whole updater contract on a fresh element", async function () {
|
|
266
|
+
await assertUpdaterContract("fresh");
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("should honor the whole updater contract again after a reconnect", async function () {
|
|
270
|
+
await assertUpdaterContract("before-reconnect");
|
|
271
|
+
await reconnect();
|
|
272
|
+
await assertUpdaterContract("after-reconnect");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("should keep the shadow root and its nodes across a reconnect", async function () {
|
|
276
|
+
const shadowRoot = element.shadowRoot;
|
|
277
|
+
const headline = query("headline");
|
|
278
|
+
const childCount = shadowRoot.childNodes.length;
|
|
279
|
+
|
|
280
|
+
await reconnect();
|
|
281
|
+
|
|
282
|
+
expect(element.shadowRoot).to.equal(shadowRoot);
|
|
283
|
+
expect(query("headline")).to.equal(headline);
|
|
284
|
+
expect(element.shadowRoot.childNodes.length).to.equal(childCount);
|
|
285
|
+
expect(element.children.length).to.equal(0);
|
|
286
|
+
expect(element.hasAttribute("data-monster-error")).to.be.false;
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("should keep option object identity across a reconnect", async function () {
|
|
290
|
+
const items = element.getOption("items");
|
|
291
|
+
const keyed = element.getOption("keyed");
|
|
292
|
+
|
|
293
|
+
element.setOption("headline", "before");
|
|
294
|
+
await settle();
|
|
295
|
+
|
|
296
|
+
await reconnect();
|
|
297
|
+
|
|
298
|
+
expect(element.getOption("items")).to.equal(items);
|
|
299
|
+
expect(element.getOption("keyed")).to.equal(keyed);
|
|
300
|
+
expect(element.getOption("headline")).to.equal("before");
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("should keep exactly one updater group linked across a reconnect", async function () {
|
|
304
|
+
function countUpdaters() {
|
|
305
|
+
let count = 0;
|
|
306
|
+
for (const group of element[customElementUpdaterLinkSymbol] ?? []) {
|
|
307
|
+
for (const updater of group) {
|
|
308
|
+
if (typeof updater?.isDisposed === "function") {
|
|
309
|
+
count++;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return count;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const before = countUpdaters();
|
|
317
|
+
expect(before).to.be.greaterThan(0);
|
|
318
|
+
|
|
319
|
+
await reconnect();
|
|
320
|
+
|
|
321
|
+
expect(countUpdaters()).to.equal(before);
|
|
322
|
+
});
|
|
323
|
+
});
|