@schukai/monster 4.70.1 → 4.70.2

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/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
 
4
4
 
5
+ ## [4.70.2] - 2026-01-03
6
+
7
+ ### Bug Fixes
8
+
9
+ - fix tests and select
10
+ ### Changes
11
+
12
+ - update tests
13
+ - update tests
14
+
15
+
16
+
5
17
  ## [4.70.1] - 2026-01-03
6
18
 
7
19
  ### Bug Fixes
package/package.json CHANGED
@@ -1 +1 @@
1
- {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.4","@popperjs/core":"^2.11.8"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.70.1"}
1
+ {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.4","@popperjs/core":"^2.11.8"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.70.2"}
@@ -2967,8 +2967,9 @@ function areOptionsAvailableAndInitInternal() {
2967
2967
  this[areOptionsAvailableAndInitSymbol]++;
2968
2968
 
2969
2969
  let options = this.getOption("options");
2970
+ const currentOptions = options;
2970
2971
 
2971
- if (isArray(options) && Array.length === 1 && isString(options?.[0])) {
2972
+ if (isArray(options) && options.length === 1 && isString(options?.[0])) {
2972
2973
  try {
2973
2974
  const obj = JSON.parse(options[0]);
2974
2975
  if (isArray(obj)) {
@@ -156,7 +156,7 @@ function getMonsterVersion() {
156
156
  }
157
157
 
158
158
  /** don't touch, replaced by make with package.json version */
159
- monsterVersion = new Version("4.70.0");
159
+ monsterVersion = new Version("4.70.1");
160
160
 
161
161
  return monsterVersion;
162
162
  }
@@ -216,6 +216,62 @@ describe('Select', function () {
216
216
 
217
217
  });
218
218
 
219
+ it('should normalize options without throwing', function (done) {
220
+ this.timeout(2000);
221
+
222
+ let mocks = document.getElementById('mocks');
223
+ const select = document.createElement('monster-select');
224
+ select.setOption('options', [
225
+ {label: 'Alpha'},
226
+ {value: 'Beta'},
227
+ {}
228
+ ]);
229
+ mocks.appendChild(select);
230
+
231
+ setTimeout(() => {
232
+ try {
233
+ const options = select.getOption('options');
234
+ expect(options[0].value).to.equal('Alpha');
235
+ expect(options[0].label).to.equal('Alpha');
236
+ expect(options[0].visibility).to.equal('visible');
237
+ expect(options[1].value).to.equal('Beta');
238
+ expect(options[1].label).to.equal('Beta');
239
+ expect(options[1].visibility).to.equal('visible');
240
+ expect(options[2].value).to.be.a('string');
241
+ expect(options[2].label).to.equal(options[2].value);
242
+ expect(options[2].visibility).to.equal('visible');
243
+ } catch (e) {
244
+ return done(e);
245
+ }
246
+
247
+ done();
248
+ }, 350);
249
+ });
250
+
251
+ it('should not parse options arrays with multiple string entries', function (done) {
252
+ this.timeout(2000);
253
+
254
+ let mocks = document.getElementById('mocks');
255
+ const select = document.createElement('monster-select');
256
+ select.setOption('options', ['One', 'Two']);
257
+ mocks.appendChild(select);
258
+
259
+ setTimeout(() => {
260
+ try {
261
+ const options = select.getOption('options');
262
+ const error = select.getAttribute('data-monster-error') ?? '';
263
+ expect(error).to.not.contain('Unexpected token');
264
+ expect(options.length).to.equal(2);
265
+ expect(options[0]).to.equal('One');
266
+ expect(options[1]).to.equal('Two');
267
+ } catch (e) {
268
+ return done(e);
269
+ }
270
+
271
+ done();
272
+ }, 350);
273
+ });
274
+
219
275
  });
220
276
 
221
277
 
@@ -8,6 +8,7 @@ describe('RestAPI', function () {
8
8
 
9
9
  let fetchReference;
10
10
  let returnStatus;
11
+ let responseBody;
11
12
 
12
13
  afterEach(() => {
13
14
  globalThis['fetch'] = fetchReference;
@@ -16,6 +17,7 @@ describe('RestAPI', function () {
16
17
  beforeEach(() => {
17
18
 
18
19
  returnStatus = 200;
20
+ responseBody = JSON.stringify({a: "test"});
19
21
  fetchReference = globalThis['fetch'];
20
22
  globalThis['fetch'] = function (options) {
21
23
 
@@ -23,9 +25,7 @@ describe('RestAPI', function () {
23
25
  resolve({
24
26
  text: function () {
25
27
  return new Promise((resolve, reject) => {
26
- resolve(JSON.stringify({
27
- a: "test"
28
- }));
28
+ resolve(responseBody);
29
29
  });
30
30
  },
31
31
  status: returnStatus
@@ -91,5 +91,42 @@ describe('RestAPI', function () {
91
91
 
92
92
  })
93
93
 
94
+ describe('read response parsing', function () {
95
+ it('should reject on invalid json', function (done) {
96
+ responseBody = "{invalid";
97
+ const ds = new RestAPI({url: 'https://monsterjs.org/assets/world.json'})
98
+ ds.read().then(() => {
99
+ done("should not run.");
100
+ }).catch(() => done());
101
+ });
102
+
103
+ it('should call responseCallback when provided', function (done) {
104
+ let called = false;
105
+ const ds = new RestAPI({
106
+ read: {
107
+ url: 'https://monsterjs.org/assets/world.json',
108
+ responseCallback: () => {
109
+ called = true;
110
+ }
111
+ }
112
+ });
113
+ ds.read().then(() => {
114
+ expect(called).to.be.true;
115
+ done();
116
+ }).catch(e => done(e));
117
+ });
118
+ });
119
+
120
+ describe('getClone', function () {
121
+ it('should return an independent clone', function () {
122
+ const ds = new RestAPI({
123
+ read: {url: 'https://monsterjs.org/assets/world.json'},
124
+ write: {url: 'https://monsterjs.org/assets/world.json'}
125
+ });
126
+ const clone = ds.getClone();
127
+ clone.setOption('read.url', 'https://example.com/changed.json');
128
+ expect(ds.getOption('read.url')).to.equal('https://monsterjs.org/assets/world.json');
129
+ });
130
+ });
94
131
 
95
132
  })
@@ -2,11 +2,10 @@ import {expect} from "chai"
2
2
  import {WebConnect} from "../../../../../source/data/datasource/server/webconnect.mjs";
3
3
  import {initWebSocket} from "../../../../util/websocket.mjs";
4
4
 
5
- const testUrl = "wss://ws.postman-echo.com/raw"
6
-
7
5
  describe('Websocket', function () {
8
6
 
9
7
  let ds = undefined
8
+ const testUrl = "ws://mock.local"
10
9
 
11
10
  before(function (done) {
12
11
  initWebSocket().then(() => {
@@ -2,6 +2,7 @@
2
2
 
3
3
  import {expect} from "chai"
4
4
  import {Datasource} from "../../../source/data/datasource.mjs";
5
+ import {DataUrl} from "../../../source/types/dataurl.mjs";
5
6
 
6
7
 
7
8
  describe('Datasource', function () {
@@ -26,6 +27,25 @@ describe('Datasource', function () {
26
27
  expect(datasource.getOption('default')).to.be.true
27
28
  });
28
29
 
30
+ it('setOptions should parse json string', function () {
31
+ const datasource = new Datasource();
32
+ datasource.setOptions('{"default":true,"nested":{"flag":1}}');
33
+ expect(datasource.getOption('default')).to.be.true
34
+ expect(datasource.getOption('nested.flag')).to.be.equal(1)
35
+ });
36
+
37
+ it('setOptions should parse data url json', function () {
38
+ const datasource = new Datasource();
39
+ const dataUrl = new DataUrl('{"default":true}', 'application/json', false).toString();
40
+ datasource.setOptions(dataUrl);
41
+ expect(datasource.getOption('default')).to.be.true
42
+ });
43
+
44
+ it('setOptions should throw on invalid json string', function () {
45
+ const datasource = new Datasource();
46
+ expect(() => datasource.setOptions('{')).to.throw(Error);
47
+ });
48
+
29
49
  })
30
50
 
31
51
  describe('rw', function () {
@@ -57,4 +77,4 @@ describe('Datasource', function () {
57
77
  })
58
78
 
59
79
 
60
- })
80
+ })
@@ -402,7 +402,10 @@ describe('DOM', function () {
402
402
  div.append(d);
403
403
 
404
404
 
405
- expect(div).contain.html('data-monster-error="value is not an instance of CSSStyleSheet"');
405
+ const cssName = CSSStyleSheet?.name || "CSSStyleSheet";
406
+ expect(div).contain.html(
407
+ `data-monster-error="value is not an instance of ${cssName}"`,
408
+ );
406
409
  done();
407
410
 
408
411
  })
@@ -54,7 +54,7 @@ describe('Stylesheet', function () {
54
54
  describe('External Stylesheet', () => {
55
55
 
56
56
  let id = new ID('Stylesheet').toString();
57
- let stylesheet, url = 'https://alvine.io/main.min.css';
57
+ let stylesheet, url = new DataUrl('body{color:red;}', 'text/css').toString();
58
58
 
59
59
  beforeEach(() => {
60
60
 
@@ -65,11 +65,20 @@ describe('Stylesheet', function () {
65
65
 
66
66
  });
67
67
 
68
+ const triggerLoad = () => {
69
+ const link = document.getElementById(id);
70
+ if (link) {
71
+ link.dispatchEvent(new window.Event("load"));
72
+ }
73
+ };
74
+
68
75
  it('append and remove Stylesheet ', (done) => {
69
76
 
70
77
  expect(stylesheet.isConnected()).to.be.false;
71
78
 
72
- stylesheet.connect().available().then(() => {
79
+ stylesheet.connect();
80
+ triggerLoad();
81
+ stylesheet.available().then(() => {
73
82
  expect(stylesheet.isConnected()).to.be.true;
74
83
  expect(document.querySelector('[href="' + url + '"]')).to.exist;
75
84
 
@@ -77,7 +86,9 @@ describe('Stylesheet', function () {
77
86
  expect(stylesheet.isConnected()).to.be.false;
78
87
  expect(document.querySelector('[href="' + url + '"]')).not.to.exist;
79
88
 
80
- stylesheet.connect().available().then(() => {
89
+ stylesheet.connect();
90
+ triggerLoad();
91
+ stylesheet.available().then(() => {
81
92
  expect(stylesheet.isConnected()).to.be.true;
82
93
  expect(document.querySelector('[href="' + url + '"]')).to.exist;
83
94
 
@@ -98,4 +109,4 @@ describe('Stylesheet', function () {
98
109
  });
99
110
 
100
111
 
101
- });
112
+ });
@@ -51,7 +51,7 @@ describe('Link', function () {
51
51
  this.timeout(5000);
52
52
 
53
53
  let id = new ID('link').toString();
54
- let link, url = 'https://alvine.io/main.min.css';
54
+ let link, url = new DataUrl('body{color:red;}', 'text/css').toString();
55
55
 
56
56
  beforeEach(() => {
57
57
 
@@ -63,11 +63,20 @@ describe('Link', function () {
63
63
 
64
64
  });
65
65
 
66
+ const triggerLoad = () => {
67
+ const node = document.getElementById(id);
68
+ if (node) {
69
+ node.dispatchEvent(new window.Event("load"));
70
+ }
71
+ };
72
+
66
73
  it('append and remove Link ', (done) => {
67
74
 
68
75
  expect(link.isConnected()).to.be.false;
69
76
 
70
- link.connect().available().then(() => {
77
+ link.connect();
78
+ triggerLoad();
79
+ link.available().then(() => {
71
80
  expect(link.isConnected()).to.be.true;
72
81
  expect(document.querySelector('[href="' + url + '"]')).to.exist;
73
82
 
@@ -75,7 +84,9 @@ describe('Link', function () {
75
84
  expect(link.isConnected()).to.be.false;
76
85
  expect(document.querySelector('[href="' + url + '"]')).not.to.exist;
77
86
 
78
- link.connect().available().then(() => {
87
+ link.connect();
88
+ triggerLoad();
89
+ link.available().then(() => {
79
90
  expect(link.isConnected()).to.be.true;
80
91
  expect(document.querySelector('[href="' + url + '"]')).to.exist;
81
92
 
@@ -96,4 +107,4 @@ describe('Link', function () {
96
107
  });
97
108
 
98
109
 
99
- });
110
+ });
@@ -60,7 +60,7 @@ describe('Script', function () {
60
60
  describe('External JS', () => {
61
61
 
62
62
  let id = new ID('script').toString();
63
- let server, script, url = 'https://cdnjs.cloudflare.com/ajax/libs/layzr.js/2.2.2/layzr.min.js';
63
+ let server, script, url = new DataUrl('console.log(1);', 'text/javascript').toString();
64
64
 
65
65
  beforeEach(() => {
66
66
 
@@ -79,11 +79,20 @@ describe('Script', function () {
79
79
 
80
80
  });
81
81
 
82
+ const triggerLoad = () => {
83
+ const node = document.getElementById(id);
84
+ if (node) {
85
+ node.dispatchEvent(new window.Event("load"));
86
+ }
87
+ };
88
+
82
89
  it('append and remove script ', (done) => {
83
90
 
84
91
  expect(script.isConnected()).to.be.false;
85
92
 
86
- script.connect().available().then(() => {
93
+ script.connect();
94
+ triggerLoad();
95
+ script.available().then(() => {
87
96
  expect(script.isConnected()).to.be.true;
88
97
  expect(document.querySelector('[src="' + url + '"]')).to.exist;
89
98
 
@@ -91,7 +100,9 @@ describe('Script', function () {
91
100
  expect(script.isConnected()).to.be.false;
92
101
  expect(document.querySelector('[src="' + url + '"]')).not.to.exist;
93
102
 
94
- script.connect().available().then(() => {
103
+ script.connect();
104
+ triggerLoad();
105
+ script.available().then(() => {
95
106
  expect(script.isConnected()).to.be.true;
96
107
  expect(document.querySelector('[src="' + url + '"]')).to.exist;
97
108
 
@@ -112,4 +123,4 @@ describe('Script', function () {
112
123
  });
113
124
 
114
125
 
115
- });
126
+ });
@@ -926,4 +926,33 @@ describe("DOM", function () {
926
926
  });
927
927
  });
928
928
  });
929
+
930
+ describe("Updater reactive updates", function () {
931
+ it("should update the DOM when the subject changes", function (done) {
932
+ let mocks = document.getElementById("mocks");
933
+ mocks.innerHTML = html1;
934
+
935
+ const element = document.getElementById("test1");
936
+ const updater = new Updater(element, { a: { b: ["one"] } });
937
+
938
+ updater
939
+ .run()
940
+ .then(() => {
941
+ expect(element).contain.html("one");
942
+
943
+ const subject = updater.getSubject();
944
+ subject.a.b = ["two"];
945
+
946
+ setTimeout(() => {
947
+ try {
948
+ expect(element).contain.html("two");
949
+ done();
950
+ } catch (e) {
951
+ done(e);
952
+ }
953
+ }, 50);
954
+ })
955
+ .catch((e) => done(e));
956
+ });
957
+ });
929
958
  });
@@ -7,7 +7,7 @@ describe('Monster', function () {
7
7
  let monsterVersion
8
8
 
9
9
  /** don´t touch, replaced by make with package.json version */
10
- monsterVersion = new Version("4.70.0")
10
+ monsterVersion = new Version("4.70.1")
11
11
 
12
12
  let m = getMonsterVersion();
13
13
 
@@ -230,4 +230,35 @@ describe('ProxyObserver', function () {
230
230
  });
231
231
  });
232
232
 
233
+ describe('deleteProperty', function () {
234
+ it('should notify observers when deleting a key', function (done) {
235
+ const proxy = new ProxyObserver({a: 1});
236
+ const observer = new Observer(function () {
237
+ expect(proxy.getRealSubject()).to.eql({});
238
+ done();
239
+ });
240
+
241
+ proxy.attachObserver(observer);
242
+ delete proxy.getSubject().a;
243
+ });
244
+ });
245
+
246
+ describe('symbol properties', function () {
247
+ it('should not notify observers for symbol keys', function (done) {
248
+ const proxy = new ProxyObserver({});
249
+ let called = false;
250
+ const observer = new Observer(function () {
251
+ called = true;
252
+ });
253
+
254
+ proxy.attachObserver(observer);
255
+ proxy.getSubject()[Symbol("hidden")] = "value";
256
+
257
+ setTimeout(() => {
258
+ expect(called).to.be.false;
259
+ done();
260
+ }, 0);
261
+ });
262
+ });
263
+
233
264
  })
@@ -2,6 +2,7 @@
2
2
 
3
3
  import {extend} from "../../source/data/extend.mjs";
4
4
  import {getGlobal} from "../../source/types/global.mjs";
5
+ import {ResizeObserverMock} from "./resize-observer.mjs";
5
6
 
6
7
  export const isBrowser = new Function("try {return this===window;}catch(e){ return false;}");
7
8
  export const isNode = new Function("try {return this===global;}catch(e){return false;}");
@@ -24,7 +25,8 @@ function initJSDOM(options) {
24
25
  includeNodeLocations: true,
25
26
  storageQuota: 10000000,
26
27
  runScripts: "dangerously",
27
- resources: "usable"
28
+ resources: "usable",
29
+ url: "https://example.test/"
28
30
  }, options || {})
29
31
 
30
32
  return import("jsdom").then(({JSDOM}) => {
@@ -84,13 +86,46 @@ function initJSDOM(options) {
84
86
  }
85
87
  });
86
88
 
87
- import("dom-storage").then(({default: Storage}) => {
88
-
89
- g.localStorage = new Storage(null, {strict: true});
90
- g.sessionStorage = new Storage(null, {strict: true});
89
+ if (!window.ResizeObserver) {
90
+ window.ResizeObserver = ResizeObserverMock;
91
+ }
92
+ if (!g.ResizeObserver) {
93
+ g.ResizeObserver = window.ResizeObserver;
94
+ }
91
95
 
92
- window['localStorage'] = g.localStorage;
93
- window['sessionStorage'] = g.sessionStorage;
96
+ import("dom-storage").then(({default: Storage}) => {
97
+ const ensureStorage = (key) => {
98
+ let existing = null;
99
+ try {
100
+ existing = window[key];
101
+ } catch (e) {}
102
+
103
+ if (existing) {
104
+ g[key] = existing;
105
+ return;
106
+ }
107
+
108
+ const storage = new Storage(null, {strict: true});
109
+ const descriptor = Object.getOwnPropertyDescriptor(window, key);
110
+
111
+ if (!descriptor || descriptor.writable || typeof descriptor.set === "function") {
112
+ window[key] = storage;
113
+ } else if (descriptor.configurable === true) {
114
+ Object.defineProperty(window, key, {
115
+ value: storage,
116
+ configurable: true,
117
+ });
118
+ }
119
+
120
+ try {
121
+ g[key] = window[key] ?? storage;
122
+ } catch (e) {
123
+ g[key] = storage;
124
+ }
125
+ };
126
+
127
+ ensureStorage("localStorage");
128
+ ensureStorage("sessionStorage");
94
129
 
95
130
  resolve(g);
96
131
 
@@ -107,4 +142,3 @@ function initJSDOM(options) {
107
142
  }
108
143
 
109
144
  export {initJSDOM, JSDOMExport}
110
-
@@ -2,28 +2,48 @@ import {getGlobal} from "../../source/types/global.mjs";
2
2
 
3
3
  function initWebSocket() {
4
4
 
5
- // const isBrowser = typeof window === 'object' && '[object Window]' === window.toString.call(window)
6
- const isNode = typeof global === 'object' && '[object global]' === global.toString.call(global)
7
-
5
+ class MockWebSocket {
6
+ constructor(url) {
7
+ this.url = url;
8
+ this.readyState = 0;
9
+ setTimeout(() => {
10
+ this.readyState = 1;
11
+ if (typeof this.onopen === "function") {
12
+ this.onopen();
13
+ }
14
+ }, 0);
15
+ }
16
+
17
+ send(data) {
18
+ if (this.readyState !== 1) {
19
+ return;
20
+ }
21
+ setTimeout(() => {
22
+ if (typeof this.onmessage === "function") {
23
+ this.onmessage({data});
24
+ }
25
+ }, 0);
26
+ }
27
+
28
+ close() {
29
+ if (this.readyState === 3) {
30
+ return;
31
+ }
32
+ this.readyState = 3;
33
+ if (typeof this.onclose === "function") {
34
+ this.onclose({code: 1000});
35
+ }
36
+ }
8
37
 
9
- if (!isNode) {
10
- return Promise.resolve();
38
+ terminate() {
39
+ this.close();
40
+ }
11
41
  }
12
42
 
13
- return import("ws").then((ws) => {
14
- getGlobal().WebSocket = class extends ws['WebSocket'] {
15
- constructor(url, protocols) {
16
- super(url, protocols, {
17
- handshakeTimeout: 1000,
18
- maxPayload: 1024 * 1024 * 1024,
19
- });
20
-
21
- }
22
- };
23
-
24
- });
43
+ getGlobal().WebSocket = MockWebSocket;
44
+ return Promise.resolve();
25
45
 
26
46
 
27
47
  }
28
48
 
29
- export {initWebSocket}
49
+ export {initWebSocket}
@@ -9,8 +9,8 @@
9
9
  </head>
10
10
  <body>
11
11
  <div id="headline" style="display: flex;align-items: center;justify-content: center;flex-direction: column;">
12
- <h1 style='margin-bottom: 0.1em;'>Monster 4.70.0</h1>
13
- <div id="lastupdate" style='font-size:0.7em'>last update Sa 3. Jan 03:58:16 CET 2026</div>
12
+ <h1 style='margin-bottom: 0.1em;'>Monster 4.70.1</h1>
13
+ <div id="lastupdate" style='font-size:0.7em'>last update Sa 3. Jan 13:33:23 CET 2026</div>
14
14
  </div>
15
15
  <div id="mocha-errors"
16
16
  style="color: red;font-weight: bold;display: flex;align-items: center;justify-content: center;flex-direction: column;margin:20px;"></div>