@schukai/monster 2.2.1 → 3.1.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.
@@ -17,7 +17,7 @@ import {
17
17
  ATTRIBUTE_UPDATER_INSERT_REFERENCE,
18
18
  ATTRIBUTE_UPDATER_REMOVE,
19
19
  ATTRIBUTE_UPDATER_REPLACE,
20
- ATTRIBUTE_UPDATER_SELECT_THIS
20
+ ATTRIBUTE_UPDATER_SELECT_THIS, customElementUpdaterLinkSymbol
21
21
  } from "../dom/constants.mjs";
22
22
 
23
23
  import {Base} from "../types/base.mjs";
@@ -27,11 +27,12 @@ import {ProxyObserver} from "../types/proxyobserver.mjs";
27
27
  import {validateArray, validateInstance} from "../types/validate.mjs";
28
28
  import {clone} from "../util/clone.mjs";
29
29
  import {trimSpaces} from "../util/trimspaces.mjs";
30
+ import {addToObjectLink} from "./attributes.mjs";
30
31
  import {findTargetElementFromEvent} from "./events.mjs";
31
32
  import {findDocumentTemplate} from "./template.mjs";
32
33
  import {getDocument} from "./util.mjs";
33
34
 
34
- export {Updater}
35
+ export {Updater, addObjectWithUpdaterToElement}
35
36
 
36
37
  /**
37
38
  * The updater class connects an object with the dom. In this way, structures and contents in the DOM can be programmatically adapted via attributes.
@@ -252,7 +253,7 @@ function getCheckStateCallback() {
252
253
  /**
253
254
  * @private
254
255
  */
255
- const symbol = Symbol('EventHandler');
256
+ const symbol = Symbol('@schukai/monster/updater@@EventHandler');
256
257
 
257
258
  /**
258
259
  * @private
@@ -308,7 +309,7 @@ function retrieveAndSetValue(element) {
308
309
  throw new Error('the bind argument must start as a value with a path');
309
310
  }
310
311
 
311
- path = path.substr(5);
312
+ path = path.substring(5);
312
313
 
313
314
  let value;
314
315
 
@@ -525,8 +526,6 @@ function insertElement(change) {
525
526
  }
526
527
 
527
528
  }
528
-
529
-
530
529
  }
531
530
 
532
531
  /**
@@ -608,8 +607,6 @@ function updateContent(change) {
608
607
  }
609
608
  }
610
609
  }
611
-
612
-
613
610
  }
614
611
 
615
612
  /**
@@ -869,3 +866,64 @@ function handleInputControlAttributeUpdate(element, name, value) {
869
866
  }
870
867
 
871
868
  }
869
+
870
+
871
+
872
+ /**
873
+ * @param {NodeList|HTMLElement|Set<HTMLElement>} elements
874
+ * @param {Symbol} symbol
875
+ * @param {object} object
876
+ * @return {Promise[]}
877
+ * @license AGPLv3
878
+ * @since 1.23.0
879
+ * @memberOf Monster.DOM
880
+ * @throws {TypeError} elements is not an instance of NodeList, HTMLElement or Set
881
+ * @throws {TypeError} the context of the function is not an instance of HTMLElement
882
+ * @throws {TypeError} symbol must be an instance of Symbol
883
+ */
884
+ function addObjectWithUpdaterToElement (elements,symbol, object) {
885
+ const self = this;
886
+ if (!(self instanceof HTMLElement)) {
887
+ throw new TypeError('the context of this function must be an instance of HTMLElement');
888
+ }
889
+
890
+ if (!(typeof symbol === 'symbol')) {
891
+ throw new TypeError('symbol must be an instance of Symbol');
892
+ }
893
+
894
+ const updaters = new Set;
895
+
896
+ if (elements instanceof NodeList) {
897
+ elements = new Set([
898
+ ...elements
899
+ ])
900
+ } else if (elements instanceof HTMLElement) {
901
+ elements = new Set([
902
+ elements
903
+ ])
904
+ } else if (elements instanceof Set) {
905
+ } else {
906
+ throw new TypeError('elements is not a valid type. (actual: ' + typeof elements + ')');
907
+ }
908
+
909
+ let result = [];
910
+
911
+ elements.forEach((element) => {
912
+ if (!(element instanceof HTMLElement)) return;
913
+ if ((element instanceof HTMLTemplateElement)) return;
914
+
915
+ const u = new Updater(element, object)
916
+ updaters.add(u);
917
+
918
+ result.push(u.run().then(() => {
919
+ return u.enableEventProcessing();
920
+ }));
921
+
922
+ });
923
+
924
+ if (updaters.size > 0) {
925
+ addToObjectLink(self, symbol, updaters);
926
+ }
927
+
928
+ return result;
929
+ }
@@ -99,7 +99,6 @@ export {
99
99
  assembleMethodSymbol,
100
100
  attributeObserverSymbol,
101
101
  registerCustomElement,
102
- assignUpdaterToElement,
103
102
  getSlottedElements
104
103
  } from "./dom/customelement.mjs"
105
104
  export {
@@ -7,11 +7,18 @@
7
7
 
8
8
  import {Base} from './base.mjs';
9
9
  import {instanceSymbol} from '../constants.mjs';
10
+
10
11
  export {Queue}
11
12
 
12
13
  /**
13
- * You can create the instance via the monster namespace `new Monster.Types.Queue()`.
14
- *
14
+ * A queue is a list of items that are processed one after another (first in, first out).
15
+ *
16
+ * With a queue you can add items to the end of the list `Queue.add()` and remove items from the beginning of the list `Queue.pop()`.
17
+ *
18
+ * With `Queue.peek()` you can get the first item without removing it from the list.
19
+ *
20
+ * You can create the instance via `new Queue()`.
21
+ *
15
22
  * @externalExample ../../example/types/queue.mjs
16
23
  * @license AGPLv3
17
24
  * @since 1.4.0
@@ -149,7 +149,7 @@ function getMonsterVersion() {
149
149
  }
150
150
 
151
151
  /** don't touch, replaced by make with package.json version */
152
- monsterVersion = new Version('2.2.1')
152
+ monsterVersion = new Version('3.1.0')
153
153
 
154
154
  return monsterVersion;
155
155
 
@@ -0,0 +1,96 @@
1
+ import {expect} from "chai"
2
+ import {WebSocketDatasource} from "../../../../../application/source/data/datasource/websocket.mjs";
3
+ import {initWebSocket} from "../../../util/websocket.mjs";
4
+
5
+ const testUrl = "wss://ws.postman-echo.com/raw"
6
+
7
+ // const g = getGlobal();
8
+ // g['WebSocket'] = WS;
9
+
10
+
11
+ describe('Websocket', function () {
12
+
13
+ let ds = undefined
14
+
15
+ before(function (done) {
16
+ initWebSocket().then(() => {
17
+ done()
18
+ }).catch((e) => {
19
+ done(e)
20
+ })
21
+ });
22
+
23
+ afterEach(function (done) {
24
+ if (ds) {
25
+ ds.close()
26
+ }
27
+
28
+ // without this, the node test will hang
29
+ for (const sym of Object.getOwnPropertySymbols(ds)) {
30
+ if (sym.toString() ==='Symbol(connection)') {
31
+ if(ds[sym]?.socket?.['terminate']) {
32
+ ds[sym]?.socket?.['terminate']()
33
+ }
34
+ }
35
+ }
36
+
37
+ done()
38
+ });
39
+
40
+ it('should connect', function (done) {
41
+ ds = new WebSocketDatasource({
42
+ url: testUrl,
43
+ reconnect: {
44
+ enabled: false
45
+ }
46
+ });
47
+ ds.connect()
48
+ setTimeout(() => {
49
+ expect(ds.isConnected()).to.be.true;
50
+ done();
51
+ }, 500);
52
+
53
+
54
+ })
55
+
56
+ it('should send message', function (done) {
57
+ ds = new WebSocketDatasource({
58
+ url: testUrl,
59
+ reconnect: {
60
+ enabled: false
61
+ }
62
+ });
63
+ ds.connect()
64
+
65
+ ds.set({
66
+ data: {
67
+ message: "Hello World"
68
+ }
69
+ })
70
+
71
+ setTimeout(() => {
72
+
73
+ ds.write().then(() => {
74
+
75
+ ds.set({})
76
+ expect(ds.get()).to.be.deep.equal({});
77
+
78
+
79
+ setTimeout(() => {
80
+
81
+ expect(ds.get()).to.be.deep.equal({
82
+ data: {
83
+ message: "Hello World"
84
+ }
85
+ });
86
+ done();
87
+ }, 1000);
88
+ }).catch((err) => {
89
+ done(new Error(err));
90
+ })
91
+ }, 1000)
92
+
93
+
94
+ }).timeout(10000);
95
+
96
+ });
@@ -2,8 +2,10 @@
2
2
 
3
3
  import chai from "chai"
4
4
  import {internalSymbol} from "../../../../application/source/constants.mjs";
5
- import {ATTRIBUTE_OPTIONS} from "../../../../application/source/dom/constants.mjs";
5
+ import {ATTRIBUTE_OPTIONS, customElementUpdaterLinkSymbol} from "../../../../application/source/dom/constants.mjs";
6
6
  import {getDocument} from "../../../../application/source/dom/util.mjs";
7
+ import {ProxyObserver} from "../../../../application/source/types/proxyobserver.mjs";
8
+ import {addObjectWithUpdaterToElement} from "../../../../application/source/dom/updater.mjs";
7
9
  import {chaiDom} from "../../util/chai-dom.mjs";
8
10
  import {initJSDOM} from "../../util/jsdom.mjs";
9
11
 
@@ -16,10 +18,164 @@ let html1 = `
16
18
  </div>
17
19
  `;
18
20
 
21
+ let html2 = `
22
+ <input data-monster-bind="path:a" id="test2" data-monster-attributes="value path:a">
23
+ `;
24
+
25
+ // defined in constants.mjs
26
+ const updaterSymbolKey = "@schukai/monster/dom/custom-element@@options-updater-link"
27
+ const updaterSymbolSymbol = Symbol.for(updaterSymbolKey);
28
+
29
+
19
30
 
20
31
  describe('DOM', function () {
21
32
 
22
- let CustomElement, registerCustomElement, TestComponent, document, TestComponent2;
33
+ let CustomElement, registerCustomElement, TestComponent, document, TestComponent2,assignUpdaterToElement;
34
+
35
+ // This allows us to inspect the addEventListener calls
36
+
37
+ // let addEventListener = EventTarget.prototype.addEventListener;
38
+ // let removeEventListener = EventTarget.prototype.removeEventListener;
39
+ //
40
+ // before(function (done) {
41
+ // initJSDOM().then(() => {
42
+ //
43
+ // EventTarget.prototype.addEventListener = function (type, callback, options) {
44
+ // /* store args and then… */
45
+ // // callback = (e) => {
46
+ // // console.log("event fired" + e);
47
+ // // callback(e);
48
+ // // };
49
+ //
50
+ // addEventListener.call(this, type, callback, options);
51
+ // };
52
+ //
53
+ // EventTarget.prototype.removeEventListener = function (type, callback, options) {
54
+ // /* remove from stored args and then… */
55
+ // removeEventListener.call(this, type, callback, options);
56
+ // };
57
+ //
58
+ // done(e);
59
+ //
60
+ //
61
+ // });
62
+ //
63
+ //
64
+ // })
65
+ //
66
+ // after(function () {
67
+ // EventTarget.prototype.addEventListener = addEventListener;
68
+ // EventTarget.prototype.removeEventListener = removeEventListener;
69
+ // });
70
+
71
+
72
+ describe("assignUpdaterToElement", function () {
73
+
74
+ before(function (done) {
75
+ initJSDOM().then(() => {
76
+ import("../../../../application/source/dom/customelement.mjs").then((m) => {
77
+ try {
78
+ CustomElement = m['CustomElement'];
79
+ assignUpdaterToElement= function (elements, object) {
80
+ return addObjectWithUpdaterToElement.call(this, elements, updaterSymbolSymbol, object);
81
+ }
82
+
83
+
84
+ document = getDocument();
85
+
86
+ done()
87
+ } catch (e) {
88
+ done(e);
89
+ }
90
+
91
+
92
+ });
93
+
94
+ });
95
+ })
96
+
97
+ beforeEach(() => {
98
+ let mocks = document.getElementById('mocks');
99
+ mocks.innerHTML = html2;
100
+ })
101
+
102
+ afterEach(() => {
103
+ let mocks = document.getElementById('mocks');
104
+ mocks.innerHTML = "";
105
+ })
106
+
107
+ /**
108
+ * this test try to simulate the bug that was found in the assignUpdaterToElement function.
109
+ * The bug was that the updater was not assigned to the element when the element was created.
110
+ *
111
+ * unfortunately, this test does not reproduce the bug.
112
+ */
113
+ it("should assign an updater to an element", function (done) {
114
+ let element = document.getElementById('test2');
115
+
116
+ expect(document.getElementById("mocks").innerHTML).to.equal(html2);
117
+
118
+ const a = {a: 1};
119
+ const b = {b: 2};
120
+
121
+ const ap = new ProxyObserver(a);
122
+ const bp = new ProxyObserver(b);
123
+
124
+ const x = ap.getSubject()
125
+ const y = bp.getSubject()
126
+
127
+ const set = new Set();
128
+ set.add(element);
129
+
130
+ assignUpdaterToElement.call(element, set, ap);
131
+ assignUpdaterToElement.call(element, set, bp);
132
+
133
+ expect(JSON.stringify(x)).to.equal('{"a":1}');
134
+ expect(JSON.stringify(y)).to.equal('{"b":2}');
135
+
136
+ const sy = updaterSymbolSymbol;
137
+
138
+ let v = element.getAttribute("data-monster-objectlink");
139
+ expect(v).to.equal('Symbol('+updaterSymbolKey+')');
140
+
141
+ const updater = element[sy];
142
+
143
+ for (const v of updater) {
144
+ for (const u of v) {
145
+ u.run().then(() => {
146
+ u.enableEventProcessing();
147
+ });
148
+ }
149
+ }
150
+
151
+ expect(updater).to.be.an.instanceof(Set);
152
+ expect(updater).to.be.a("Set");
153
+
154
+ x.a = 3;
155
+ bp.getSubject().b = 4;
156
+
157
+ setTimeout(() => {
158
+
159
+ let mockHTML = document.getElementById("mocks");
160
+
161
+ // html expexted:
162
+ // <input data-monster-bind="path:a" id="test2" data-monster-attributes="value path:a" data-monster-objectlink="Symbol(@schukai/monster/dom/@@object-updater-link)" value="3">
163
+
164
+ expect(mockHTML.querySelector("#test2")).to.have.value('3')
165
+ expect(mockHTML.querySelector("#test2")).to.have.attribute('data-monster-objectlink', 'Symbol('+updaterSymbolKey+')')
166
+ //expect(mockHTML).to.have.html(resultHTML);
167
+
168
+ expect(element.value).to.equal("3");
169
+
170
+ expect(JSON.stringify(ap.getRealSubject())).to.equal('{"a":3}');
171
+ expect(JSON.stringify(bp.getRealSubject())).to.equal('{"b":4}');
172
+ done()
173
+ }, 10)
174
+
175
+ })
176
+
177
+ })
178
+
23
179
 
24
180
  describe('CustomElement()', function () {
25
181
 
@@ -102,7 +258,7 @@ describe('DOM', function () {
102
258
 
103
259
  let monster = document.getElementById('thisisatest');
104
260
  expect(monster.getOption('demotest')).is.eql(1425);
105
-
261
+
106
262
  });
107
263
  });
108
264
 
@@ -133,7 +289,7 @@ describe('DOM', function () {
133
289
  try {
134
290
  expect(document.getElementsByTagName('monster-testclass2').length).is.equal(1);
135
291
  expect(document.getElementsByTagName('monster-testclass2').item(0).shadowRoot.innerHTML).is.equal('<h1></h1><article><p>test</p><div id="container"></div></article>');
136
- expect(document.getElementById('test1')).contain.html('<monster-testclass2 data-monster-objectlink="Symbol(@schukai/monster/dom/@@object-updater-link)"></monster-testclass2>');
292
+ expect(document.getElementById('test1')).contain.html('<monster-testclass2 data-monster-objectlink="Symbol('+updaterSymbolKey+')"></monster-testclass2>');
137
293
  return done();
138
294
  } catch (e) {
139
295
  done(e);
@@ -514,6 +670,4 @@ describe('DOM', function () {
514
670
  })
515
671
 
516
672
  });
517
- });
518
-
519
-
673
+ })
@@ -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('2.2.1')
10
+ monsterVersion = new Version('3.1.0')
11
11
 
12
12
  let m = getMonsterVersion();
13
13
 
@@ -9,7 +9,7 @@ describe('DeadMansSwitch', function () {
9
9
 
10
10
  const ms1 = Date.now();
11
11
 
12
- const deadmansswitch = new DeadMansSwitch(100, () => {
12
+ new DeadMansSwitch(100, () => {
13
13
 
14
14
  const ms2 = Date.now();
15
15
 
@@ -38,12 +38,12 @@ describe('DeadMansSwitch', function () {
38
38
  const diff = ms2 - ms1;
39
39
 
40
40
  if (ms1 > ms2) {
41
- done('timing error');
41
+ done(new Error('timing error'));
42
42
  return;
43
43
  }
44
44
 
45
45
  if (diff < 600) {
46
- done('to short ' + diff);
46
+ done(new Error('to short ' + diff));
47
47
  return;
48
48
  }
49
49
  done();
@@ -54,6 +54,7 @@ function initJSDOM(options) {
54
54
  'Document',
55
55
  'Node',
56
56
  'ShadowRoot',
57
+ 'EventTarget',
57
58
  'Event',
58
59
  'CustomEvent',
59
60
  'Element',
@@ -62,6 +63,7 @@ function initJSDOM(options) {
62
63
  'customElements',
63
64
  'DocumentFragment',
64
65
  'DOMParser',
66
+ 'KeyboardEvent',
65
67
  'CSSStyleSheet',
66
68
  'HTMLScriptElement',
67
69
  'MutationObserver',
@@ -0,0 +1,22 @@
1
+ import {getGlobal} from "../../../application/source/types/global.mjs";
2
+
3
+ function initWebSocket() {
4
+ if (typeof window === "object" && window['WebSocket']) return Promise.resolve();
5
+
6
+ return import("ws").then((ws) => {
7
+ getGlobal().WebSocket = class extends ws['WebSocket'] {
8
+ constructor(url, protocols) {
9
+ super(url, protocols, {
10
+ handshakeTimeout: 1000,
11
+ maxPayload: 1024 * 1024 * 1024,
12
+ });
13
+
14
+ }
15
+ };
16
+
17
+ });
18
+
19
+
20
+ }
21
+
22
+ export {initWebSocket}
@@ -14,6 +14,7 @@ import "../cases/dom/updater.mjs";
14
14
  import "../cases/dom/customcontrol.mjs";
15
15
  import "../cases/dom/locale.mjs";
16
16
  import "../cases/dom/theme.mjs";
17
+ import "../cases/dom/resource.mjs";
17
18
  import "../cases/dom/resourcemanager.mjs";
18
19
  import "../cases/dom/util.mjs";
19
20
  import "../cases/dom/customelement.mjs";
@@ -58,6 +59,7 @@ import "../cases/constraint/invalid.mjs";
58
59
  import "../cases/constraint/andoperator.mjs";
59
60
  import "../cases/constraint/oroperator.mjs";
60
61
  import "../cases/constraint/isarray.mjs";
62
+ import "../cases/constraint/abstractoperator.mjs";
61
63
  import "../cases/constraint/valid.mjs";
62
64
  import "../cases/util/trimspaces.mjs";
63
65
  import "../cases/util/deadmansswitch.mjs";
@@ -76,4 +78,5 @@ import "../cases/data/diff.mjs";
76
78
  import "../cases/data/datasource/restapi.mjs";
77
79
  import "../cases/data/datasource/storage/sessionstorage.mjs";
78
80
  import "../cases/data/datasource/storage/localstorage.mjs";
81
+ import "../cases/data/datasource/webservice.mjs";
79
82
  import "../cases/math/random.mjs";
@@ -5,7 +5,7 @@
5
5
  <title>Mocha Monster</title>
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
7
7
  <link rel="stylesheet" href="mocha.css"/>
8
- <script id="polyfill" src="https://polyfill.io/v3/polyfill.min.js?features=Array.from,Array.isArray,Array.prototype.entries,Array.prototype.fill,Array.prototype.filter,Array.prototype.forEach,Array.prototype.indexOf,Array.prototype.keys,Array.prototype.lastIndexOf,Array.prototype.map,Array.prototype.reduce,Array.prototype.sort,ArrayBuffer,atob,CustomEvent,DataView,document,Document,DocumentFragment,Element,Event,fetch,globalThis,HTMLDocument,HTMLTemplateElement,Intl,JSON,Map,Math.log2,Number.isInteger,Object.assign,Object.defineProperty,Object.entries,Object.freeze,Object.getOwnPropertyDescriptor,Object.getOwnPropertyNames,Object.getOwnPropertySymbols,Object.getPrototypeOf,Object.keys,Promise,Reflect,Reflect.defineProperty,Reflect.get,Reflect.getOwnPropertyDescriptor,Reflect.setPrototypeOf,Set,String.prototype.endsWith,String.prototype.matchAll,String.prototype.padStart,String.prototype.startsWith,String.prototype.trim,Symbol,Symbol.for,Symbol.hasInstance,Symbol.iterator,Uint16Array,Uint8Array,URL,WeakMap,WeakSet"
8
+ <script id="polyfill" src="https://polyfill.io/v3/polyfill.min.js?features=Array.from,Array.isArray,Array.prototype.entries,Array.prototype.fill,Array.prototype.filter,Array.prototype.forEach,Array.prototype.indexOf,Array.prototype.keys,Array.prototype.lastIndexOf,Array.prototype.map,Array.prototype.reduce,Array.prototype.sort,ArrayBuffer,atob,CustomEvent,DataView,document,Document,DocumentFragment,Element,Event,fetch,globalThis,HTMLDocument,HTMLTemplateElement,Intl,JSON,Map,Math.log2,Number.isInteger,Object.assign,Object.defineProperty,Object.entries,Object.freeze,Object.getOwnPropertyDescriptor,Object.getOwnPropertyNames,Object.getPrototypeOf,Object.keys,Promise,Reflect,Reflect.defineProperty,Reflect.get,Reflect.getOwnPropertyDescriptor,Reflect.setPrototypeOf,Set,String.prototype.endsWith,String.prototype.matchAll,String.prototype.padStart,String.prototype.startsWith,String.prototype.trim,Symbol,Symbol.for,Symbol.hasInstance,Symbol.iterator,Uint16Array,Uint8Array,URL,WeakMap,WeakSet"
9
9
  src="https://polyfill.io/v3/polyfill.min.js?features=Array.from,Array.isArray,Array.prototype.entries,Array.prototype.fill,Array.prototype.forEach,Array.prototype.indexOf,Array.prototype.keys,Array.prototype.lastIndexOf,Array.prototype.map,Array.prototype.reduce,Array.prototype.sort,ArrayBuffer,atob,DataView,document,DocumentFragment,Element,Event,globalThis,HTMLDocument,HTMLTemplateElement,JSON,Map,Math.log2,Number.isInteger,Object.assign,Object.defineProperty,Object.entries,Object.getOwnPropertyDescriptor,Object.getPrototypeOf,Object.keys,Promise,Reflect,Reflect.defineProperty,Reflect.get,Reflect.getOwnPropertyDescriptor,Reflect.setPrototypeOf,Set,String.prototype.endsWith,String.prototype.matchAll,String.prototype.padStart,String.prototype.startsWith,String.prototype.trim,Symbol,Symbol.iterator,WeakMap,WeakSet"
10
10
  crossorigin="anonymous"
11
11
  referrerpolicy="no-referrer"></script>
@@ -14,8 +14,8 @@
14
14
  </head>
15
15
  <body>
16
16
  <div id="headline" style="display: flex;align-items: center;justify-content: center;flex-direction: column;">
17
- <h1 style='margin-bottom: 0.1em;'>Monster 2.0.16</h1>
18
- <div id="lastupdate" style='font-size:0.7em'>last update Do 29. Dez 14:29:19 CET 2022</div>
17
+ <h1 style='margin-bottom: 0.1em;'>Monster 3.0.0</h1>
18
+ <div id="lastupdate" style='font-size:0.7em'>last update Fr 6. Jan 12:54:47 CET 2023</div>
19
19
  </div>
20
20
  <div id="mocks"></div>
21
21
  <div id="mocha"></div>