@schukai/monster 4.152.0 → 4.153.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/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/dom/resource.mjs +19 -5
- package/source/dom/sanitize-html.mjs +34 -29
- 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/transformer.mjs +7 -0
- package/test/cases/dom/customcontrol.mjs +2 -6
- 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/updater.mjs +1 -1
- 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
|
+
});
|
|
@@ -30,6 +30,13 @@ describe('Transformer', function () {
|
|
|
30
30
|
|
|
31
31
|
const isNode = typeof global === 'object' && '[object global]' === global.toString.call(global);
|
|
32
32
|
|
|
33
|
+
let previousLanguage;
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
previousLanguage = document.documentElement.lang;
|
|
36
|
+
document.documentElement.lang = isNode ? 'en-US' : 'en-GB';
|
|
37
|
+
});
|
|
38
|
+
afterEach(() => { document.documentElement.lang = previousLanguage; });
|
|
39
|
+
|
|
33
40
|
|
|
34
41
|
[
|
|
35
42
|
['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 () {
|
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
|
+
});
|
|
@@ -780,7 +780,7 @@ describe("DOM", function () {
|
|
|
780
780
|
|
|
781
781
|
[false, true].forEach((batchUpdates) => {
|
|
782
782
|
it(`should bulk insert and reuse indexed entries with batchUpdates=${batchUpdates} issue #548`, async function () {
|
|
783
|
-
this.timeout(
|
|
783
|
+
this.timeout(20000);
|
|
784
784
|
const mocks = document.getElementById("mocks");
|
|
785
785
|
mocks.innerHTML = `
|
|
786
786
|
<template id="bulk-row">
|
|
@@ -12,7 +12,7 @@ describe('Processing', function () {
|
|
|
12
12
|
let t = +new Date();
|
|
13
13
|
|
|
14
14
|
const desiredDelay = 200;
|
|
15
|
-
this.timeout(
|
|
15
|
+
this.timeout(2000);
|
|
16
16
|
|
|
17
17
|
// delay 12
|
|
18
18
|
new Processing((v) => {
|
|
@@ -33,7 +33,7 @@ describe('Processing', function () {
|
|
|
33
33
|
expect(counter).to.be.equal(4);
|
|
34
34
|
expect(v === 'test').to.be.true;
|
|
35
35
|
// check delay
|
|
36
|
-
expect(+new Date()
|
|
36
|
+
expect(+new Date() >= t + desiredDelay).to.be.true;
|
|
37
37
|
} catch (e) {
|
|
38
38
|
return done(e);
|
|
39
39
|
}
|
|
@@ -107,4 +107,4 @@ describe('Processing', function () {
|
|
|
107
107
|
|
|
108
108
|
});
|
|
109
109
|
|
|
110
|
-
});
|
|
110
|
+
});
|