@schukai/monster 4.152.0 → 4.153.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -1
- package/package.json +1 -1
- package/source/components/content/viewer/message.mjs +1 -0
- package/source/components/form/select.mjs +2 -0
- package/source/components/form/util/fetch.mjs +6 -7
- package/source/components/state/log.mjs +7 -0
- package/source/components/state/thread.mjs +11 -0
- package/source/components/time/month-calendar.mjs +80 -30
- package/source/data/buildmap.mjs +3 -2
- package/source/data/buildtree.mjs +0 -8
- package/source/data/pathfinder.mjs +48 -19
- package/source/data/transformer.mjs +2 -2
- package/source/dom/customcontrol.mjs +35 -9
- package/source/dom/customelement.mjs +21 -2
- package/source/dom/resource.mjs +19 -5
- package/source/dom/sanitize-html.mjs +34 -29
- package/source/dom/slotted.mjs +46 -31
- package/source/dom/template.mjs +4 -1
- package/source/dom/updater.mjs +51 -15
- package/source/types/node.mjs +1 -3
- package/source/types/proxyobserver.mjs +32 -10
- package/test/cases/components/datatable/drag-scroll.mjs +28 -1
- package/test/cases/components/form/button-bar.mjs +9 -6
- package/test/cases/components/form/confirm-button.mjs +4 -4
- package/test/cases/components/form/control-bar.mjs +73 -72
- package/test/cases/components/form/input-group.mjs +3 -1
- package/test/cases/components/form/password.mjs +2 -2
- package/test/cases/components/form/popper-button.mjs +2 -2
- package/test/cases/components/form/select-sort-callback.mjs +57 -0
- package/test/cases/components/form/select.mjs +2 -2
- package/test/cases/components/form/sheet.mjs +13 -1
- package/test/cases/components/layout/popper.mjs +3 -1
- package/test/cases/components/layout/tabs.mjs +4 -1
- package/test/cases/components/navigation/site-navigation.mjs +12 -26
- package/test/cases/components/navigation/table-of-content.mjs +3 -0
- package/test/cases/components/state/ticker-lifecycle.mjs +47 -0
- package/test/cases/components/time/month-calendar.mjs +100 -0
- package/test/cases/data/buildmap.mjs +14 -0
- package/test/cases/data/buildtree.mjs +49 -0
- package/test/cases/data/pathfinder.mjs +34 -0
- package/test/cases/data/transformer.mjs +33 -0
- package/test/cases/dom/customcontrol.mjs +87 -6
- package/test/cases/dom/customelement.mjs +54 -0
- package/test/cases/dom/ready.mjs +3 -3
- package/test/cases/dom/resource-availability.mjs +46 -0
- package/test/cases/dom/resourcemanager.mjs +12 -6
- package/test/cases/dom/sanitize-html.mjs +71 -0
- package/test/cases/dom/slotted-nodes.mjs +48 -1
- package/test/cases/dom/template.mjs +11 -0
- package/test/cases/dom/updater.mjs +119 -1
- package/test/cases/types/proxyobserver.mjs +75 -0
- package/test/cases/util/processing.mjs +3 -3
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { expect } from "chai";
|
|
2
|
+
import { initJSDOM } from "../../../util/jsdom.mjs";
|
|
3
|
+
import { ATTRIBUTE_ERRORMESSAGE } from "../../../../source/dom/constants.mjs";
|
|
4
|
+
|
|
5
|
+
describe("MonthCalendar date configuration", () => {
|
|
6
|
+
let MonthCalendar;
|
|
7
|
+
before(async () => {
|
|
8
|
+
await initJSDOM();
|
|
9
|
+
({ MonthCalendar } = await import(
|
|
10
|
+
"../../../../source/components/time/month-calendar.mjs"
|
|
11
|
+
));
|
|
12
|
+
});
|
|
13
|
+
afterEach(() => document.getElementById("mocks").replaceChildren());
|
|
14
|
+
const updates = () => new Promise((resolve) => setTimeout(resolve, 30));
|
|
15
|
+
const currentDays = (calendar) =>
|
|
16
|
+
calendar.getOption("calendarDays").filter((day) => day.isCurrentMonth);
|
|
17
|
+
it("applies JSON options and refreshes the rendered grid after setOption", async () => {
|
|
18
|
+
const mocks = document.getElementById("mocks");
|
|
19
|
+
mocks.innerHTML =
|
|
20
|
+
'<monster-month-calendar data-monster-options=\'{"startDate":"2024-02-01"}\'></monster-month-calendar>';
|
|
21
|
+
const calendar = mocks.firstElementChild;
|
|
22
|
+
await updates();
|
|
23
|
+
expect(currentDays(calendar)).to.have.length(29);
|
|
24
|
+
expect(currentDays(calendar)[0].day).to.equal("2024-02-01");
|
|
25
|
+
calendar.setOption("startDate", "2026-09-01");
|
|
26
|
+
await updates();
|
|
27
|
+
expect(currentDays(calendar)).to.have.length(30);
|
|
28
|
+
expect(
|
|
29
|
+
calendar.shadowRoot.querySelector('[data-monster-day="2026-09-01"]'),
|
|
30
|
+
).to.not.equal(null);
|
|
31
|
+
calendar.setAttribute("data-monster-option-start-date", "2026-10-01");
|
|
32
|
+
await updates();
|
|
33
|
+
expect(currentDays(calendar)[0].day).to.equal("2026-10-01");
|
|
34
|
+
});
|
|
35
|
+
it("keeps the last valid grid for invalid input and recovers without clearing other errors", async () => {
|
|
36
|
+
const calendar = document.createElement("monster-month-calendar");
|
|
37
|
+
calendar.setOption("startDate", "2026-03-01");
|
|
38
|
+
document.getElementById("mocks").append(calendar);
|
|
39
|
+
await updates();
|
|
40
|
+
calendar.setAttribute(ATTRIBUTE_ERRORMESSAGE, "unrelated");
|
|
41
|
+
for (const invalid of ["not-a-date", "2026-02-30", null, new Date(NaN)]) {
|
|
42
|
+
calendar.setOption("startDate", invalid);
|
|
43
|
+
await updates();
|
|
44
|
+
expect(currentDays(calendar)[0].day).to.equal("2026-03-01");
|
|
45
|
+
expect(calendar.getAttribute(ATTRIBUTE_ERRORMESSAGE)).to.contain(
|
|
46
|
+
"Invalid calendar date",
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
calendar.setOption("startDate", new Date(2026, 10, 1));
|
|
50
|
+
await updates();
|
|
51
|
+
expect(currentDays(calendar)[0].day).to.equal("2026-11-01");
|
|
52
|
+
expect(calendar.getAttribute(ATTRIBUTE_ERRORMESSAGE)).to.contain(
|
|
53
|
+
"unrelated",
|
|
54
|
+
);
|
|
55
|
+
expect(calendar.getAttribute(ATTRIBUTE_ERRORMESSAGE)).not.to.contain(
|
|
56
|
+
"Invalid calendar date",
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
it("constructs invalid-default subclasses with a usable local-month fallback", async () => {
|
|
60
|
+
class InvalidCalendar extends MonthCalendar {
|
|
61
|
+
get defaults() {
|
|
62
|
+
return { ...super.defaults, startDate: null };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
customElements.define("monster-test-invalid-calendar", InvalidCalendar);
|
|
66
|
+
const calendar = document.createElement("monster-test-invalid-calendar");
|
|
67
|
+
expect(calendar).to.be.instanceOf(InvalidCalendar);
|
|
68
|
+
document.getElementById("mocks").append(calendar);
|
|
69
|
+
await updates();
|
|
70
|
+
expect(currentDays(calendar)[0].month).to.equal(new Date().getMonth() + 1);
|
|
71
|
+
expect(calendar.getOption("calendarDays")).to.have.length(42);
|
|
72
|
+
});
|
|
73
|
+
it("keeps day interactions and weekday updates working after changing months", async () => {
|
|
74
|
+
const calendar = document.createElement("monster-month-calendar");
|
|
75
|
+
document.getElementById("mocks").append(calendar);
|
|
76
|
+
calendar.setOption("startDate", "2026-09-01");
|
|
77
|
+
await updates();
|
|
78
|
+
calendar.setOption("startDate", "2026-10-01");
|
|
79
|
+
calendar.setOption("locale.weekdayFormat", "long");
|
|
80
|
+
await updates();
|
|
81
|
+
const cell = calendar.shadowRoot.querySelector(
|
|
82
|
+
'[data-monster-day="2026-10-01"]',
|
|
83
|
+
);
|
|
84
|
+
// JSDOM has no hit testing; browsers use their native implementation.
|
|
85
|
+
calendar.shadowRoot.elementFromPoint ??= () => null;
|
|
86
|
+
const more = cell.querySelector('[data-monster-role="more-appointments"]');
|
|
87
|
+
more.dispatchEvent(
|
|
88
|
+
new window.MouseEvent("click", { bubbles: true, view: window }),
|
|
89
|
+
);
|
|
90
|
+
expect(
|
|
91
|
+
cell.querySelector('[data-monster-role="appointment-popper"]').style
|
|
92
|
+
.display,
|
|
93
|
+
).to.equal("block");
|
|
94
|
+
expect(calendar.getOption("calendarWeekdays")[0].label).to.equal("Monday");
|
|
95
|
+
const days = calendar.getOption("calendarDays");
|
|
96
|
+
calendar.setOption("labels.__update", true);
|
|
97
|
+
await updates();
|
|
98
|
+
expect(calendar.getOption("calendarDays")).to.equal(days);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -6,6 +6,20 @@ import {buildMap} from "../../../source/data/buildmap.mjs";
|
|
|
6
6
|
|
|
7
7
|
describe('buildMap', function () {
|
|
8
8
|
|
|
9
|
+
describe('falsy keys and literal replacements issue #569', function () {
|
|
10
|
+
it('should preserve a zero key returned by a selector callback', function () {
|
|
11
|
+
const value = {label: 'zero'};
|
|
12
|
+
const map = buildMap({}, () => new Map([[0, value]]), '${label}');
|
|
13
|
+
expect([...map]).to.deep.equal([[0, 'zero']]);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should insert replacement tokens literally', function () {
|
|
17
|
+
const value = {label: '$&-$`-$\''};
|
|
18
|
+
const map = buildMap({}, () => new Map([['key', value]]), '${label}');
|
|
19
|
+
expect(map.get('key')).to.equal('$&-$`-$\'');
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
9
23
|
let convertMapResult = function (r) {
|
|
10
24
|
if (r instanceof Map) {
|
|
11
25
|
r = Object.fromEntries(r);
|
|
@@ -170,6 +170,55 @@ describe('buildTree', function () {
|
|
|
170
170
|
|
|
171
171
|
});
|
|
172
172
|
|
|
173
|
+
describe('parent metadata issue #568', function () {
|
|
174
|
+
const createTree = (records) => buildTree(
|
|
175
|
+
records,
|
|
176
|
+
(values) => new Map(values.map((value, index) => [index, value])),
|
|
177
|
+
'id',
|
|
178
|
+
'parent',
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
[
|
|
182
|
+
[
|
|
183
|
+
{id: 1, parent: null},
|
|
184
|
+
{id: 2, parent: 1},
|
|
185
|
+
{id: 3, parent: 2},
|
|
186
|
+
],
|
|
187
|
+
[
|
|
188
|
+
{id: 3, parent: 2},
|
|
189
|
+
{id: 2, parent: 1},
|
|
190
|
+
{id: 1, parent: null},
|
|
191
|
+
],
|
|
192
|
+
].forEach((records, index) => {
|
|
193
|
+
it(`should retain immediate parents independent of input order ${index + 1}`, function () {
|
|
194
|
+
const tree = createTree(records);
|
|
195
|
+
const root = [...tree][0];
|
|
196
|
+
const child = [...root.childNodes][0];
|
|
197
|
+
const leaf = [...child.childNodes][0];
|
|
198
|
+
|
|
199
|
+
expect(root.parent).to.equal(null);
|
|
200
|
+
expect(root.level).to.equal(0);
|
|
201
|
+
expect(child.parent).to.equal(root);
|
|
202
|
+
expect(child.level).to.equal(1);
|
|
203
|
+
expect(leaf.parent).to.equal(child);
|
|
204
|
+
expect(leaf.level).to.equal(2);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should preserve frozen input records and their identity', function () {
|
|
209
|
+
const rootValue = Object.freeze({id: 1, parent: null});
|
|
210
|
+
const childValue = Object.freeze({id: 2, parent: 1});
|
|
211
|
+
const tree = createTree(Object.freeze([childValue, rootValue]));
|
|
212
|
+
const root = [...tree][0];
|
|
213
|
+
const child = [...root.childNodes][0];
|
|
214
|
+
|
|
215
|
+
expect(root.value).to.equal(rootValue);
|
|
216
|
+
expect(child.value).to.equal(childValue);
|
|
217
|
+
expect(Reflect.ownKeys(rootValue)).to.deep.equal(['id', 'parent']);
|
|
218
|
+
expect(Reflect.ownKeys(childValue)).to.deep.equal(['id', 'parent']);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
173
222
|
describe('buildTree() test 2', function () {
|
|
174
223
|
|
|
175
224
|
/**
|
|
@@ -17,6 +17,8 @@ describe('Pathfinder', function () {
|
|
|
17
17
|
`constructor.prototype.${probe}`,
|
|
18
18
|
['__proto__', probe],
|
|
19
19
|
['constructor', 'prototype', probe],
|
|
20
|
+
[`safe.__proto__`, probe],
|
|
21
|
+
[`safe.constructor.prototype`, probe],
|
|
20
22
|
].forEach(function (path) {
|
|
21
23
|
it(`setVia(${JSON.stringify(path)}) should reject unsafe mutation paths`, function () {
|
|
22
24
|
try {
|
|
@@ -108,6 +110,38 @@ describe('Pathfinder', function () {
|
|
|
108
110
|
expect(() => new Pathfinder({values}).setVia('values.next', {}))
|
|
109
111
|
.to.throw(Error, 'unsupported action for this data type in setValueViaPath');
|
|
110
112
|
});
|
|
113
|
+
|
|
114
|
+
it('exists() should use collection membership semantics', function () {
|
|
115
|
+
expect(new Pathfinder(new Map([['entry', 0]])).exists('entry')).to.equal(true);
|
|
116
|
+
expect(new Pathfinder(new Map()).exists('entry')).to.equal(false);
|
|
117
|
+
expect(new Pathfinder(new Set([false])).exists('0')).to.equal(true);
|
|
118
|
+
expect(new Pathfinder(new Set()).exists('0')).to.equal(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('wildcards should enumerate Map and Set values', function () {
|
|
122
|
+
expect([...new Pathfinder(new Map([['entry', false]])).getVia('*')])
|
|
123
|
+
.to.deep.equal([['entry', false]]);
|
|
124
|
+
expect([...new Pathfinder(new Set(['first', 'second'])).getVia('*')])
|
|
125
|
+
.to.deep.equal([['0', 'first'], ['1', 'second']]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('collection indices should reject trailing characters', function () {
|
|
129
|
+
expect(() => new Pathfinder(['first', 'second']).getVia('1suffix'))
|
|
130
|
+
.to.throw(TypeError);
|
|
131
|
+
expect(() => new Pathfinder(new Set(['first', 'second'])).getVia('1suffix'))
|
|
132
|
+
.to.throw(TypeError);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should support null-prototype objects', function () {
|
|
136
|
+
const subject = Object.assign(Object.create(null), {present: false});
|
|
137
|
+
const pathfinder = new Pathfinder(subject);
|
|
138
|
+
|
|
139
|
+
expect(pathfinder.getVia('present')).to.equal(false);
|
|
140
|
+
expect(pathfinder.exists('present')).to.equal(true);
|
|
141
|
+
expect(pathfinder.exists('missing')).to.equal(false);
|
|
142
|
+
pathfinder.setVia('next', 0);
|
|
143
|
+
expect(subject.next).to.equal(0);
|
|
144
|
+
});
|
|
111
145
|
});
|
|
112
146
|
|
|
113
147
|
let convertMapResult = function (r) {
|
|
@@ -8,6 +8,32 @@ import {initJSDOM} from "../../util/jsdom.mjs";
|
|
|
8
8
|
|
|
9
9
|
describe('Transformer', function () {
|
|
10
10
|
|
|
11
|
+
describe('falsy property values issue #569', function () {
|
|
12
|
+
[
|
|
13
|
+
['key:value:fallback', {value: 0}, 0],
|
|
14
|
+
['property:value:fallback', {value: false}, false],
|
|
15
|
+
['index:0:fallback', [''], ''],
|
|
16
|
+
['key:value:fallback', {value: null}, null],
|
|
17
|
+
['first-key:fallback', {a: false}, false],
|
|
18
|
+
['last-key:fallback', {a: 1, z: 0}, 0],
|
|
19
|
+
].forEach(([definition, value, expected]) => {
|
|
20
|
+
it(`${definition} should preserve ${String(expected)}`, function () {
|
|
21
|
+
expect(new Transformer(definition).run(value)).to.equal(expected);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should distinguish inherited and missing properties', function () {
|
|
26
|
+
const inherited = Object.create({value: false});
|
|
27
|
+
expect(new Transformer('key:value:fallback').run(inherited)).to.equal(false);
|
|
28
|
+
expect(new Transformer('key:missing:fallback').run(inherited)).to.equal('fallback');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should support null-prototype objects', function () {
|
|
32
|
+
const value = Object.assign(Object.create(null), {entry: 0});
|
|
33
|
+
expect(new Transformer('key:entry:fallback').run(value)).to.equal(0);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
11
37
|
before(function (done) {
|
|
12
38
|
|
|
13
39
|
let promises = []
|
|
@@ -30,6 +56,13 @@ describe('Transformer', function () {
|
|
|
30
56
|
|
|
31
57
|
const isNode = typeof global === 'object' && '[object global]' === global.toString.call(global);
|
|
32
58
|
|
|
59
|
+
let previousLanguage;
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
previousLanguage = document.documentElement.lang;
|
|
62
|
+
document.documentElement.lang = isNode ? 'en-US' : 'en-GB';
|
|
63
|
+
});
|
|
64
|
+
afterEach(() => { document.documentElement.lang = previousLanguage; });
|
|
65
|
+
|
|
33
66
|
|
|
34
67
|
[
|
|
35
68
|
['datetimeformat', "2023-02-04 08:02:01", "Feb 4, 2023, 08:02:01", "4 Feb 2023, 08:02:01"],
|
|
@@ -19,11 +19,7 @@ describe('DOM', function () {
|
|
|
19
19
|
let CustomControl, registerCustomElement, TestComponent, document, jsdomFlag;
|
|
20
20
|
|
|
21
21
|
before(function (done) {
|
|
22
|
-
initJSDOM({}).then(() => {
|
|
23
|
-
|
|
24
|
-
import("element-internals-polyfill").then((m) => {
|
|
25
|
-
m.polyfill();
|
|
26
|
-
});
|
|
22
|
+
initJSDOM({}).then(() => import("element-internals-polyfill")).then(() => {
|
|
27
23
|
|
|
28
24
|
// jsdom does not support ElementInternals
|
|
29
25
|
jsdomFlag = navigator.userAgent.includes("jsdom");
|
|
@@ -56,7 +52,7 @@ describe('DOM', function () {
|
|
|
56
52
|
}).catch((e) => {
|
|
57
53
|
done(e);
|
|
58
54
|
});
|
|
59
|
-
});
|
|
55
|
+
}).catch(done);
|
|
60
56
|
})
|
|
61
57
|
|
|
62
58
|
describe('formDisabledCallback()', function () {
|
|
@@ -388,6 +384,91 @@ describe('DOM', function () {
|
|
|
388
384
|
control.remove();
|
|
389
385
|
});
|
|
390
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
|
+
|
|
391
472
|
it('should replay a pre-upgrade value once after assembly issue #501', async function () {
|
|
392
473
|
const htmlTAG = 'monster-customcontrol-pre-upgrade';
|
|
393
474
|
const control = document.createElement(htmlTAG);
|
|
@@ -587,6 +587,60 @@ describe('DOM', function () {
|
|
|
587
587
|
linkedUpdater[internalSymbol].subject.detachObserver(observer);
|
|
588
588
|
}
|
|
589
589
|
});
|
|
590
|
+
|
|
591
|
+
it('should remove stale updater keys after replacing an option object issue #565', async function () {
|
|
592
|
+
const htmlTAG = 'monster-testclass-object-sync-565';
|
|
593
|
+
|
|
594
|
+
if (!customElements.get(htmlTAG)) {
|
|
595
|
+
class ObjectSyncComponent extends CustomElement {
|
|
596
|
+
static getTag() {
|
|
597
|
+
return htmlTAG;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
get defaults() {
|
|
601
|
+
return Object.assign({}, super.defaults, {
|
|
602
|
+
settings: {a: 'initial', b: 'retained'},
|
|
603
|
+
templates: {
|
|
604
|
+
main: '<span data-monster-replace="path:settings.a"></span>',
|
|
605
|
+
},
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
registerCustomElement(ObjectSyncComponent);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const element = document.createElement(htmlTAG);
|
|
614
|
+
document.getElementById('test1').appendChild(element);
|
|
615
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
616
|
+
|
|
617
|
+
let linkedUpdater;
|
|
618
|
+
for (const group of element[customElementUpdaterLinkSymbol]) {
|
|
619
|
+
for (const updater of group) {
|
|
620
|
+
linkedUpdater = updater;
|
|
621
|
+
break;
|
|
622
|
+
}
|
|
623
|
+
if (linkedUpdater) break;
|
|
624
|
+
}
|
|
625
|
+
expect(linkedUpdater).to.exist;
|
|
626
|
+
|
|
627
|
+
element.setOptions({settings: {a: 'merged'}});
|
|
628
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
629
|
+
expect(linkedUpdater.getSubject().settings).to.deep.include({
|
|
630
|
+
a: 'merged',
|
|
631
|
+
b: 'retained',
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
const settingsProxy = linkedUpdater.getSubject().settings;
|
|
635
|
+
element.setOption('settings', {a: 'replacement'});
|
|
636
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
637
|
+
|
|
638
|
+
expect(linkedUpdater.getSubject().settings).to.equal(settingsProxy);
|
|
639
|
+
expect(linkedUpdater.getSubject().settings).to.deep.equal({
|
|
640
|
+
a: 'replacement',
|
|
641
|
+
});
|
|
642
|
+
expect(element.shadowRoot.textContent).to.equal('replacement');
|
|
643
|
+
});
|
|
590
644
|
})
|
|
591
645
|
|
|
592
646
|
describe('setOptions()', function () {
|
package/test/cases/dom/ready.mjs
CHANGED
|
@@ -25,7 +25,7 @@ describe('Ready', function () {
|
|
|
25
25
|
describe('domReady', function () {
|
|
26
26
|
|
|
27
27
|
it('resolve promise', function (done) {
|
|
28
|
-
domReady.then(done).catch(e => done(e));
|
|
28
|
+
domReady.then(() => done()).catch(e => done(e));
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
});
|
|
@@ -33,10 +33,10 @@ describe('Ready', function () {
|
|
|
33
33
|
describe('windowReady', function () {
|
|
34
34
|
|
|
35
35
|
it('resolve promise', function (done) {
|
|
36
|
-
windowReady.then(done).catch(e => done(e));
|
|
36
|
+
windowReady.then(() => done()).catch(e => done(e));
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
});
|
|
40
40
|
|
|
41
41
|
|
|
42
|
-
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { expect } from "chai";
|
|
2
|
+
import { initJSDOM } from "../../util/jsdom.mjs";
|
|
3
|
+
import { Script } from "../../../source/dom/resource/script.mjs";
|
|
4
|
+
import { referenceSymbol } from "../../../source/dom/resource.mjs";
|
|
5
|
+
import { internalStateSymbol } from "../../../source/constants.mjs";
|
|
6
|
+
|
|
7
|
+
describe("Resource availability", () => {
|
|
8
|
+
let resource;
|
|
9
|
+
before(() => initJSDOM());
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
resource = new Script({
|
|
12
|
+
src: "/resource-test.js",
|
|
13
|
+
type: "application/json",
|
|
14
|
+
timeout: 30,
|
|
15
|
+
}).connect();
|
|
16
|
+
});
|
|
17
|
+
afterEach(() => resource[referenceSymbol].remove());
|
|
18
|
+
const outcome = (promise) =>
|
|
19
|
+
promise.then(
|
|
20
|
+
() => "loaded",
|
|
21
|
+
(error) => error,
|
|
22
|
+
);
|
|
23
|
+
for (const event of ["load", "error"]) {
|
|
24
|
+
it(`settles every pending and late consumer consistently on ${event}`, async () => {
|
|
25
|
+
const pending = Array.from({ length: 3 }, () =>
|
|
26
|
+
outcome(resource.available()),
|
|
27
|
+
);
|
|
28
|
+
resource[referenceSymbol].dispatchEvent(new Event(event));
|
|
29
|
+
const results = await Promise.all(pending);
|
|
30
|
+
const late = await outcome(resource.available());
|
|
31
|
+
for (const result of results) expect(result).to.equal(late);
|
|
32
|
+
if (event === "load") expect(late).to.equal("loaded");
|
|
33
|
+
else expect(late).to.contain("is not available");
|
|
34
|
+
expect(resource[internalStateSymbol].observers.observers).to.have.length(
|
|
35
|
+
0,
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
it("detaches timed-out consumers and still permits a later successful wait", async () => {
|
|
40
|
+
expect(await outcome(resource.available())).to.equal("timeout");
|
|
41
|
+
expect(resource[internalStateSymbol].observers.observers).to.have.length(0);
|
|
42
|
+
const next = resource.available();
|
|
43
|
+
resource[referenceSymbol].dispatchEvent(new Event("load"));
|
|
44
|
+
await next;
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -93,16 +93,22 @@ describe('ResourceManager', function () {
|
|
|
93
93
|
|
|
94
94
|
|
|
95
95
|
describe('check availability resources', function () {
|
|
96
|
-
it('add script and check availability should return Promise', function () {
|
|
97
|
-
|
|
96
|
+
it('add script and check availability should return Promise', async function () {
|
|
97
|
+
const available = manager.addScript('/example.js').available();
|
|
98
|
+
expect(available).to.be.instanceOf(Promise);
|
|
99
|
+
expect(await available.then(() => 'unexpected success', String)).to.equal('no element');
|
|
98
100
|
});
|
|
99
101
|
|
|
100
|
-
it('add style and check availability should should return Promise', function () {
|
|
101
|
-
|
|
102
|
+
it('add style and check availability should should return Promise', async function () {
|
|
103
|
+
const available = manager.addStylesheet('/style.js').available();
|
|
104
|
+
expect(available).to.be.instanceOf(Promise);
|
|
105
|
+
expect(await available.then(() => 'unexpected success', String)).to.equal('no element');
|
|
102
106
|
});
|
|
103
107
|
|
|
104
|
-
it('add data and check availability should should return Promise', function () {
|
|
105
|
-
|
|
108
|
+
it('add data and check availability should should return Promise', async function () {
|
|
109
|
+
const available = manager.addData(validDataURL, {allowedOrigins}).available();
|
|
110
|
+
expect(available).to.be.instanceOf(Promise);
|
|
111
|
+
expect(await available.then(() => 'unexpected success', String)).to.equal('no element');
|
|
106
112
|
});
|
|
107
113
|
})
|
|
108
114
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { expect } from "chai";
|
|
2
|
+
import { initJSDOM } from "../../util/jsdom.mjs";
|
|
3
|
+
import { sanitizeHtml } from "../../../source/dom/sanitize-html.mjs";
|
|
4
|
+
import { sanitizeViewerHtml } from "../../../source/components/content/viewer/sanitize-html.mjs";
|
|
5
|
+
|
|
6
|
+
describe("HTML sanitization", () => {
|
|
7
|
+
before(() => initJSDOM());
|
|
8
|
+
for (const [name, sanitize] of [
|
|
9
|
+
["general", sanitizeHtml],
|
|
10
|
+
["viewer", sanitizeViewerHtml],
|
|
11
|
+
]) {
|
|
12
|
+
it(`${name}: removes executable and obfuscated links`, () => {
|
|
13
|
+
const root = document.createElement("div");
|
|
14
|
+
root.innerHTML = sanitize(
|
|
15
|
+
'<a href="jav	ascript:window.bad=true" onclick="window.bad=true">one</a><a href=" DATA:text/html,test">two</a><svg><a xlink:href="vbscript:test">three</a></svg>',
|
|
16
|
+
);
|
|
17
|
+
for (const link of root.querySelectorAll("a")) {
|
|
18
|
+
expect(link.hasAttribute("href")).to.equal(false);
|
|
19
|
+
expect(link.hasAttribute("xlink:href")).to.equal(false);
|
|
20
|
+
expect(link.hasAttribute("onclick")).to.equal(false);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
it(`${name}: removes active SVG while preserving static graphics and formatting`, () => {
|
|
24
|
+
const root = document.createElement("div");
|
|
25
|
+
root.innerHTML = sanitize(
|
|
26
|
+
'<strong>Text</strong><table><tbody><tr><td>Cell</td></tr></tbody></table><svg viewBox="0 0 10 10"><path d="M0 0h10v10z" fill="red"/><a><animate attributeName="href" values="javascript:test"/><set attributeName="href" to="javascript:test"/><animateMotion path="M0 0L1 1"/><animateTransform attributeName="transform"/></a></svg>',
|
|
27
|
+
);
|
|
28
|
+
expect(root.querySelector("strong").textContent).to.equal("Text");
|
|
29
|
+
expect(root.querySelector("td").textContent).to.equal("Cell");
|
|
30
|
+
expect(root.querySelector("path").getAttribute("d")).to.equal(
|
|
31
|
+
"M0 0h10v10z",
|
|
32
|
+
);
|
|
33
|
+
expect(
|
|
34
|
+
[...root.querySelectorAll("*")].some((element) =>
|
|
35
|
+
["animate", "set", "animatemotion", "animatetransform"].includes(
|
|
36
|
+
element.localName.toLowerCase(),
|
|
37
|
+
),
|
|
38
|
+
),
|
|
39
|
+
).to.equal(false);
|
|
40
|
+
});
|
|
41
|
+
it(`${name}: preserves safe links and local images`, () => {
|
|
42
|
+
for (const href of [
|
|
43
|
+
"https://example.test/path",
|
|
44
|
+
"http://example.test",
|
|
45
|
+
"/path",
|
|
46
|
+
"../path",
|
|
47
|
+
"#section",
|
|
48
|
+
"mailto:test@example.test",
|
|
49
|
+
"tel:+49123",
|
|
50
|
+
]) {
|
|
51
|
+
const root = document.createElement("div");
|
|
52
|
+
root.innerHTML = sanitize(
|
|
53
|
+
`<a href="${href}">Link</a><img src="/image.png" alt="Image">`,
|
|
54
|
+
);
|
|
55
|
+
expect(root.querySelector("a").getAttribute("href")).to.equal(href);
|
|
56
|
+
expect(root.querySelector("img").getAttribute("src")).to.equal(
|
|
57
|
+
"/image.png",
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
it("sanitizes nested template contents without removing safe templates", () => {
|
|
63
|
+
const root = document.createElement("div");
|
|
64
|
+
root.innerHTML = sanitizeHtml(
|
|
65
|
+
'<div><template><a href="javascript:test">Link</a><script>test</script></template></div>',
|
|
66
|
+
);
|
|
67
|
+
const content = root.querySelector("template").content;
|
|
68
|
+
expect(content.querySelector("a").hasAttribute("href")).to.equal(false);
|
|
69
|
+
expect(content.querySelector("script")).to.equal(null);
|
|
70
|
+
});
|
|
71
|
+
});
|