als-document 1.0.5-alpha → 1.0.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "als-document",
3
- "version": "1.0.5-alpha",
3
+ "version": "1.0.5",
4
4
  "description": "A powerful HTML parser & DOM manipulation library for both backend and frontend.",
5
5
  "main": "index.js",
6
6
  "module": "index.mjs",
@@ -10,8 +10,5 @@
10
10
  },
11
11
  "keywords": ["HTML", "DOM", "parser", "manipulation", "backend", "frontend", "als-document"],
12
12
  "author": "Alex Sorkin <alexsorkin1980@gmail.com>",
13
- "license": "ISC",
14
- "devDependencies": {
15
- "als-simple-test": "^0.3.5"
16
- }
13
+ "license": "ISC"
17
14
  }
package/readme.md CHANGED
@@ -1,9 +1,14 @@
1
1
  # als-document: HTML Parser & DOM Manipulation Library
2
2
 
3
+
3
4
  ## Overview
4
5
 
5
6
  `als-document` is a powerful library for parsing HTML and manipulating the DOM structure on backend and frontend. It provides a robust and intuitive API for querying and interacting with DOM elements using selectors, making it a valuable tool for web developers.
6
7
 
8
+ ## Release notes
9
+ * als-document is still on alpha testing. All tested features works fine, but through the use, discovering some bugs or things that should work different. For example in this release, changed the way for storing attributes with empty value.
10
+ * Also, this release, has additional very powefull feature which is building cache for storing DOM tree as json and building back DOM from cache.
11
+
7
12
 
8
13
  ## Installation
9
14
 
@@ -21,13 +26,13 @@ The library provides three different files to cater to different module systems:
21
26
  1. **index.js**: This file uses the CommonJS module system. It's suitable for projects using Node.js or bundlers like Browserify or Webpack. The entry point in `package.json` for this file is "main".
22
27
 
23
28
  ```javascript
24
- const { parseHTML, Node, Query, TextNode, SingleNode } = require('als-document');
29
+ const { parseHTML, Node, Query, TextNode, SingleNode,Root } = require('als-document');
25
30
  ```
26
31
 
27
32
  2. **index.mjs**: This file uses the ES Modules (ESM) system. It's suitable for modern JavaScript environments that support ESM. The entry point in `package.json` for this file is "module".
28
33
 
29
34
  ```js
30
- import { parseHTML, Node, Query, TextNode, SingleNode } from 'als-document';
35
+ import { parseHTML, Node, Query, TextNode, SingleNode, Root } from 'als-document';
31
36
  ```
32
37
 
33
38
  3. **document.js**: By including this file, a constant variable named `alsDocument` is created, which wraps all the exports.
@@ -35,7 +40,7 @@ import { parseHTML, Node, Query, TextNode, SingleNode } from 'als-document';
35
40
  ```html
36
41
  <script src="/node_modules/als-document/document.js"></script>
37
42
  <script>
38
- const { parseHTML, Node, Query, TextNode, SingleNode } = alsDocument
43
+ const { parseHTML, Node, Query, TextNode, SingleNode, buildFromCache, cacheDoc, Root } = alsDocument
39
44
  </script>
40
45
  ```
41
46
 
@@ -101,6 +106,7 @@ Remember, the actual tree structure will be more complex and detailed, but the p
101
106
  - **getElementsByClassName, getElementsByTagName, getElementById**: Get elements by class, tag, or id respectively.
102
107
  - **insertAdjacentElement, insertAdjacentHTML, insertAdjacentText**: Insert content relative to the element.
103
108
  - **appendChild**: Add a child node to the element.
109
+ - **insert(place,element)**: place (0-3) or beforebegin,afterbegin,... eleemnt - raw html or element
104
110
 
105
111
 
106
112
  ### SingleNode
@@ -112,6 +118,15 @@ Remember, the actual tree structure will be more complex and detailed, but the p
112
118
  `TextNode` is a class that represents text content within the DOM. A TextNode holds raw text data and does not have child nodes.
113
119
 
114
120
 
121
+ ### Root node (extends Node)
122
+
123
+ Has additional getters and setters:
124
+ * getter root.title
125
+ * setter root.title
126
+ * getter root.body
127
+ * getter root.head
128
+
129
+
115
130
 
116
131
  ### Examples:
117
132
 
@@ -242,3 +257,18 @@ if attribute has value, attrib object will contain check function with one param
242
257
  let s = Query.get('[test^="some"]')[0]
243
258
  console.log(s.attribs[0].check('some value test')) // true
244
259
  ```
260
+
261
+ ## buildFromCache and cacheDoc
262
+
263
+ Building DOM from raw html, usually takes tens of milliseconds. But now, you can build DOM once and save it's cache as regular stringified JSON.
264
+ The caching process and building from cache takes less then 5ms for each and require realy low resources.
265
+
266
+
267
+ How it works?
268
+ ```js
269
+ const html = `` // some real html 255KB
270
+ const root = parseHTML(html); // 31.9ms
271
+ const cache = cacheDoc(root); // 2.4ms
272
+ const root1 = buildFromCache(cache); // 1.2ms
273
+ console.log(root.inneHTML === root1.innerHTML) // true
274
+ ```
package/src/build.js CHANGED
@@ -22,9 +22,9 @@ const files = {
22
22
  'query': ['query','check-element'],
23
23
  'node':[
24
24
  'dataset','find','text-node',
25
- 'style','class-list','node','single-node'
25
+ 'style','class-list','node','single-node','root'
26
26
  ],
27
- 'parse':['parse-atts','void-tags','parser'],
27
+ 'parse':['parse-atts','void-tags','parser','cache'],
28
28
  }
29
29
  const fileList = []
30
30
  function buildFileList() {
@@ -40,7 +40,7 @@ buildFileList()
40
40
  function build() {
41
41
  let content = fileList.map(filePath => readFileSync(filePath, 'utf-8')).join('\n');
42
42
 
43
- const toReturn = '{ parseHTML, Node, Query, TextNode, SingleNode }'
43
+ const toReturn = '{ parseHTML, Node, Query, TextNode, SingleNode, buildFromCache, cacheDoc, Root }'
44
44
  content = optimizeCode(content)
45
45
  writeFileSync(join(root, 'document.js'), `const alsDocument = (function(){\n${content}\nreturn ${toReturn}\n})()`)
46
46
  writeFileSync(join(root, 'index.js'), content + '\n' + `module.exports = ${toReturn}`)
package/src/node/node.js CHANGED
@@ -19,10 +19,10 @@ class Node {
19
19
 
20
20
  get id() { return this.attributes.id ? this.attributes.id : null; }
21
21
  set id(newValue) { this.attributes.id = newValue; }
22
- get className() {return this.attributes.class || null}
22
+ get className() { return this.attributes.class || null }
23
23
  get parentNode() { return this.parent }
24
24
  get ancestors() {
25
- if(!this.parent) return []
25
+ if (!this.parent) return []
26
26
  const ancestors = []
27
27
  let element = this.parent
28
28
  while (element.tagName !== 'ROOT') {
@@ -33,13 +33,13 @@ class Node {
33
33
  }
34
34
 
35
35
  get childNodeIndex() {
36
- if(!this.parent) return null
37
- return this.parent.childNodes ? this.parent.childNodes.indexOf(this) : null
36
+ if (!this.parent) return null
37
+ return this.parent.childNodes ? this.parent.childNodes.indexOf(this) : null
38
38
  }
39
39
 
40
- get childIndex() {
41
- if(!this.parent) return null
42
- return this.parent.children ? this.parent.children.indexOf(this) : null
40
+ get childIndex() {
41
+ if (!this.parent) return null
42
+ return this.parent.children ? this.parent.children.indexOf(this) : null
43
43
  }
44
44
  get previousElementSibling() { return this.prev }
45
45
  get prev() {
@@ -69,8 +69,8 @@ class Node {
69
69
  }
70
70
 
71
71
  get outerHTML() {
72
- const attrs = Object.entries(this.attributes).map(([key, val]) => `${key}="${val}"`).join(" ");
73
- return `<${this.tagName}${attrs ? ' '+attrs : ''}>${this.innerHTML}</${this.tagName}>`;
72
+ const attrs = Object.entries(this.attributes).map(([key, val]) => val.length ? `${key}="${val}"` : key).join(" ");
73
+ return `<${this.tagName}${attrs ? ' ' + attrs : ''}>${this.innerHTML}</${this.tagName}>`;
74
74
  }
75
75
 
76
76
  getAttribute(attrName) { return this.attributes[attrName] || null }
@@ -79,7 +79,7 @@ class Node {
79
79
 
80
80
  remove() {
81
81
  if (!this.parent) return
82
- const index = this.childIndex;
82
+ const index = this.childNodeIndex;
83
83
  if (index !== null) this.parent.childNodes.splice(index, 1);
84
84
  }
85
85
 
@@ -91,12 +91,12 @@ class Node {
91
91
  }).join("");
92
92
  }
93
93
 
94
- $$(query) {return this.querySelectorAll(query)}
94
+ $$(query) { return this.querySelectorAll(query) }
95
95
  querySelectorAll(query) {
96
96
  const selectors = Query.get(query)
97
97
  return find(selectors, this, new Set())
98
98
  }
99
- $(query) {return this.querySelector(query)}
99
+ $(query) { return this.querySelector(query) }
100
100
  querySelector(query) {
101
101
  const selectors = Query.get(query)
102
102
  return find(selectors, this, new Set(), true)[0] || null
@@ -115,12 +115,15 @@ class Node {
115
115
  }
116
116
 
117
117
  insertAdjacentElement(position, newElement) {
118
- if(newElement.tagName === 'ROOT' && newElement.childNodes.length > 0) newElement = newElement.childNodes[0]
118
+ if (newElement.tagName === 'ROOT' && newElement.childNodes.length > 0) newElement = newElement.childNodes[0]
119
119
  const pos = position.toLowerCase();
120
- if (pos === "afterbegin") this.childNodes.unshift(newElement);
121
- else if (pos === "beforeend") this.childNodes.push(newElement);
122
- newElement.parent = this
123
- if (!this.parent) return newElement
120
+ if (pos === 'afterbegin' || pos === 'beforeend') {
121
+ if (pos === "afterbegin") this.childNodes.unshift(newElement);
122
+ else if (pos === "beforeend") this.childNodes.push(newElement);
123
+ newElement.parent = this
124
+ return newElement
125
+ }
126
+ if (!this.parent) throw new Error("Can't insert element to element without parent")
124
127
  if (pos === "beforebegin") insertBefore(this.parent.childNodes, this.childNodeIndex, newElement)
125
128
  else if (pos === "afterend") this.parent.childNodes.splice(this.childNodeIndex + 1, 0, newElement);
126
129
  newElement.parent = this.parent
@@ -139,9 +142,26 @@ class Node {
139
142
  return this.insertAdjacentElement(position, new TextNode(text));
140
143
  }
141
144
 
145
+ insert(position, element) {
146
+ const positions = ['beforebegin', 'afterbegin', 'beforeend', 'afterend']
147
+ if (positions[position]) position = positions[position]
148
+ if (typeof element === 'string') {
149
+ element = element.trim()
150
+ if (element.startsWith('<') && element.endsWith('>')) {
151
+ return this.insertAdjacentHTML(position, element)
152
+ }
153
+ return this.insertAdjacentText(position, element)
154
+ }
155
+ return this.insertAdjacentElement(position, element)
156
+ }
157
+
142
158
  set innerHTML(html) {
143
- const parsed = parseHTML(html);
144
- this.childNodes = parsed.childNodes;
159
+ if (this.tagName === 'script' || this.tagName === 'style') {
160
+ this.childNodes.length ? this.childNodes[0] = html : this.childNodes.push(html)
161
+ } else {
162
+ const parsed = parseHTML(html);
163
+ this.childNodes = parsed.childNodes;
164
+ }
145
165
  }
146
166
 
147
167
  set outerHTML(html) {
@@ -154,7 +174,7 @@ class Node {
154
174
  appendChild(newChild) {
155
175
  if (newChild instanceof Node || newChild instanceof TextNode || newChild instanceof SingleNode) {
156
176
  if (newChild.parent) newChild.parent.childNodes = newChild.parent.childNodes.filter(child => child !== newChild); // Если у newChild уже есть родительский узел, необходимо его удалить оттуда
157
- } else if(typeof newChild === 'string') newChild = new TextNode(newChild)
177
+ } else if (typeof newChild === 'string') newChild = new TextNode(newChild)
158
178
  else return newChild
159
179
  this.childNodes.push(newChild);
160
180
  newChild.parent = this;
@@ -164,9 +184,9 @@ class Node {
164
184
  get textContent() {
165
185
  if (this.childNodes.length === 0) return this.nodeName === '#text' ? this.nodeValue : '';
166
186
  return this.childNodes.map(child => { // Concatenate text content of this node and all descendants
167
- if(child instanceof SingleNode) return ''
168
- if(child instanceof TextNode) return child.nodeValue
169
- if(child instanceof Node) return child.textContent;
187
+ if (child instanceof SingleNode) return ''
188
+ if (child instanceof TextNode) return child.nodeValue
189
+ if (child instanceof Node) return child.textContent;
170
190
  else return child;
171
191
  }).join(" ");
172
192
  }
@@ -0,0 +1,11 @@
1
+ class Root extends Node {
2
+ constructor() {
3
+ super('ROOT',{},null);
4
+ this.isSingle = false
5
+ }
6
+
7
+ get body() {return this.$('body')}
8
+ get head() {return this.$('head')}
9
+ get title() {return this.$('title')}
10
+ set title(title) {return this.$('title').innerHTML = title}
11
+ }
@@ -5,12 +5,12 @@ class SingleNode extends Node {
5
5
  this.isSingle = true
6
6
  }
7
7
 
8
- get outerHTML() { // Переопределение outerHTML для одиночного узла
8
+ get outerHTML() { // outerHTML for single node
9
9
  if (this.tagName === "#cdata-section") return `<![CDATA[${this.nodeValue}]]>`;
10
- const attrs = Object.entries(this.attributes).map(([key, val]) => `${key}="${val}"`).join(" ");
10
+ const attrs = Object.entries(this.attributes).map(([key, val]) => val.length ? `${key}="${val}"` : key).join(" ");
11
11
  return `<${this.tagName} ${attrs}${this.tagName === '?xml' ? '?' : ''}>`;
12
12
  }
13
- // Удаляем методы и свойства, которые не имеют смысла для одиночных узлов
13
+ // Remove getters,setters and methods which no make sence in single node
14
14
  get innerHTML() { return ""; }
15
15
  set innerHTML(_) { }
16
16
  $(_) {return null}
@@ -25,6 +25,7 @@ class SingleNode extends Node {
25
25
  insertAdjacentHTML(_, __) { }
26
26
  insertAdjacentText(_, __) { }
27
27
  appendChild(_) { }
28
+ insert(_,__) { }
28
29
  get textContent() { return ""; }
29
30
  set textContent(_) { }
30
- }
31
+ }
@@ -0,0 +1,33 @@
1
+ function buildFromCache(cached) {
2
+ function buildNode(cache,parent=null) {
3
+ if(typeof cache === 'string') return parent.childNodes.push(cache)
4
+ const {isSingle,tagName,attributes,childNodes,textContent} = cache
5
+ if(textContent) return parent.childNodes.push(new TextNode(textContent))
6
+ if(isSingle) return parent.childNodes.push(new SingleNode(tagName,attributes))
7
+ const newDoc = tagName === 'ROOT' ? new Root() : new Node(tagName,attributes,parent)
8
+ childNodes.forEach(childNode => {
9
+ buildNode(childNode,newDoc)
10
+ });
11
+ return newDoc
12
+ }
13
+ return buildNode(cached)
14
+ }
15
+
16
+
17
+ function cacheDoc(doc) {
18
+ const props = ['isSingle','tagName','attributes']
19
+ function addToCache(element,cache={}) {
20
+ if(typeof element === 'string') return element
21
+ if(element.nodeName === '#text') return {textContent:element.textContent}
22
+ props.forEach(prop => {
23
+ if(element[prop]) cache[prop] = element[prop]
24
+ });
25
+ if(!element.childNodes) return cache
26
+ cache.childNodes = []
27
+ element.childNodes.forEach(childNode => {
28
+ cache.childNodes.push(addToCache(childNode))
29
+ });
30
+ return cache
31
+ }
32
+ return addToCache(doc)
33
+ }
@@ -31,6 +31,6 @@ function parseAttributes(str) {
31
31
  else value += char;
32
32
  }
33
33
 
34
- if (key.trim() && !value) attrs[key.trim()] = true; // After the loop, check if there's a leftover key, which would be an attribute without a value
34
+ if (key.trim() && !value) attrs[key.trim()] = ''; // After the loop, check if there's a leftover key, which would be an attribute without a value
35
35
  return attrs;
36
36
  }
@@ -1,5 +1,5 @@
1
1
  function parseHTML(html) {
2
- const root = new Node("ROOT");
2
+ const root = new Root();
3
3
  const stack = [root];
4
4
  let currentText = "", i = 0;
5
5
  let max = 0
@@ -52,7 +52,9 @@ function parseHTML(html) {
52
52
 
53
53
  if (html.startsWith("<", i)) {
54
54
  if (currentText && stack[stack.length - 1]) {
55
- stack[stack.length - 1].childNodes.push(new TextNode(currentText));
55
+ const textNode = new TextNode(currentText)
56
+ stack[stack.length - 1].childNodes.push(textNode);
57
+ textNode.parent = stack[stack.length - 1]
56
58
  currentText = "";
57
59
  }
58
60
 
@@ -31,7 +31,7 @@ class Query {
31
31
 
32
32
  splitAndCutLast(string, splitBy) {
33
33
  const array = string.split(splitBy);
34
- const last = array.pop(); // pop() удаляет и возвращает последний элемент массива
34
+ const last = array.pop();
35
35
  return [last, array];
36
36
  }
37
37
 
@@ -104,11 +104,18 @@ class Query {
104
104
  attribs = attribs.map(attrib => {
105
105
  let query = attrib
106
106
  attrib = attrib.replace('[', '').replace(']', '')
107
- let [name, value] = attrib.split(/[\~\|\^\$\*]?\=/)
108
- let sign = attrib.replace(name, '').replace(value, '')
109
- attrib = { query }
110
- if (name) attrib.name = name
111
- if (value) attrib.value = value.trim().replace(/^\"/, '').replace(/\"$/, '')
107
+ let [name,...values] = attrib.split('=')
108
+ const value = values.join('=').trim().replace(/^\"/,'').replace(/\"$/,'')
109
+ let sign
110
+ attrib = {query,name}
111
+ if(value) {
112
+ sign = '='
113
+ attrib.name = attrib.name.replace(/[\~\|\^\$\*]$/,(match => {
114
+ sign = match+sign
115
+ return ''
116
+ }))
117
+ attrib.value = value
118
+ }
112
119
  if (sign) {
113
120
  attrib.sign = sign
114
121
  attrib.check = this.getAttribFn(sign).bind(attrib)
package/tests/cache.js ADDED
@@ -0,0 +1,19 @@
1
+ function mesureTime(fn) {
2
+ let time = performance.now()
3
+ const result = fn()
4
+ time = performance.now() - time
5
+ return {result,time}
6
+ }
7
+
8
+ describe('Cache and build from cache', () => {
9
+ const {result:root,time:rootTime} = mesureTime(() => parseHTML(html1))
10
+ const {result:cache,time:cacheTime} = mesureTime(() => cacheDoc(root))
11
+ const {result:root1,time:root1Time} = mesureTime(() => buildFromCache(cache))
12
+
13
+ it('HTML from cache and HTML from not cached same',() => {
14
+ console.log({rootTime,cacheTime,root1Time})
15
+ assert(cacheTime < 5,'Build cache takes less then 5ms')
16
+ assert(root1Time < 5,'Build DOM from cache takes less then 5ms')
17
+ assert(root.innerHTML === root1.innerHTML)
18
+ })
19
+ })
package/tests/index.html CHANGED
@@ -4,13 +4,13 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Document</title>
7
- <script src="/node_modules/als-simple-test/test.js"></script>
7
+ <script src="test.js"></script>
8
8
  <script src="../document.js"></script>
9
- <script src="./data/html1.js"></script>
10
- <!-- <script src="./data/html2.js"></script> -->
9
+ <!-- <script src="./data/html1.js"></script> -->
10
+ <script src="./data/html2.js"></script>
11
11
  <script src="./data/svg.js"></script>
12
12
  <script>
13
- const { parseHTML, Node, Query, TextNode, SingleNode } = alsDocument
13
+ const { parseHTML, Node, Query, TextNode, SingleNode, buildFromCache, cacheDoc,Root } = alsDocument
14
14
  let {describe,it,beforeEach,runTests,expect,delay,assert,beforeAll} = SimpleTest
15
15
  SimpleTest.showFullError = true
16
16
  </script>
@@ -18,6 +18,7 @@
18
18
  <script src="./query.js"></script>
19
19
  <script src="parser.js"></script>
20
20
  <script src="node.js"></script>
21
+ <script src="./cache.js"></script>
21
22
  </head>
22
23
  <body>
23
24
  <script src="parse-real.js"></script>
@@ -32,8 +32,9 @@ describe('Real data html1', async () => {
32
32
  it('Text nodes check', () => {
33
33
  const realParagraph = iframe.querySelector('p');
34
34
  const parsedParagraph = parsedHTML.querySelector('p');
35
- const real = realParagraph.textContent.trim().replace(/\n\s*/gm,'')
36
- const parsed = parsedParagraph.textContent.trim()
35
+ const real = realParagraph.textContent.trim().replace(/\n|\s/gm,'')
36
+ const parsed = parsedParagraph.textContent.trim().replace(/\n|\s/gm,'')
37
+ console.log({parsed,real})
37
38
  assert(real === parsed, 'Text contents are the same');
38
39
  });
39
40
 
package/tests/parser.js CHANGED
@@ -70,7 +70,7 @@ describe('Advanced tests', () => {
70
70
  let time = Date.now() - now
71
71
  // console.log(memoryAfter - memoryBefore)
72
72
  assert(time < 20, `Big html (${(deepHTML.length / 1024).toFixed(2)}KB) in less then 20ms (${time}ms)`)
73
- expect(result).instanceof(Node); // or any other validation you see fit
73
+ expect(result).instanceof(Root); // or any other validation you see fit
74
74
  });
75
75
 
76
76
  it('handles incorrectly closed tags', () => {
package/tests/test.js ADDED
@@ -0,0 +1,169 @@
1
+ class SimpleTest {
2
+ static tests = [];
3
+ static beforeEachCallback;
4
+ static afterEachCallback;
5
+ static nestingLevel = 0;
6
+ static currentParent = [];
7
+ static space = ''
8
+ static results = {}
9
+ static testTitle = ''
10
+ static showFullError = false
11
+ static colors = {
12
+ bold:'\x1b[1m',
13
+ cblue:'\x1b[34m',
14
+ cred:'\x1b[31m',
15
+ cgreen:'\x1b[32m',
16
+ cgray:'\x1b[37m',
17
+ reset:'\x1b[0m'
18
+ }
19
+ static delay = (ms,toResolve) => new Promise((resolve) => {
20
+ setTimeout(() => {resolve(toResolve)}, ms);
21
+ });
22
+
23
+ static describe(title, callback,pause=false) {
24
+ if(pause) return
25
+ let { nestingLevel, currentParent } = SimpleTest
26
+ nestingLevel++;
27
+ currentParent.push(title);
28
+ callback();
29
+
30
+ currentParent.pop();
31
+ nestingLevel--;
32
+ if (nestingLevel === 0) {
33
+ SimpleTest.beforeEachCallback = null; // Clear beforeEachCallback after finishing the outermost describe block
34
+ SimpleTest.afterEachCallback = null; // Clear beforeEachCallback after finishing the outermost describe block
35
+ }
36
+ }
37
+
38
+ static beforeEach(callback) {SimpleTest.beforeEachCallback = callback;}
39
+ static afterEach(callback) {SimpleTest.afterEachCallback = callback;}
40
+ static beforeAll(callback) {SimpleTest.beforAllCallback = callback;}
41
+ static afterAll(callback) {SimpleTest.afterAllCallback = callback;}
42
+
43
+ static it(title, callback) {
44
+ SimpleTest.tests.push({
45
+ titles: [...SimpleTest.currentParent, title],
46
+ before: SimpleTest.beforeEachCallback,
47
+ after:SimpleTest.afterEachCallback,
48
+ test: callback,
49
+ });
50
+ }
51
+
52
+ static async runTests(consoleLog=true) {
53
+ const {bold,cblue,cred,reset} = SimpleTest.colors
54
+ SimpleTest.consoleLog = consoleLog
55
+ let { tests,results,showFullError,beforAllCallback,afterAllCallback } = SimpleTest;
56
+ let previousTitles = [];
57
+ if(typeof beforAllCallback == 'function') await beforAllCallback()
58
+ for(const test of tests) {
59
+ let curentResult = results
60
+ SimpleTest.space = test.titles.map(t => ' ').join('');
61
+ test.titles.forEach((title, index) => {
62
+ if(curentResult[title] == undefined) {
63
+ if(index == test.titles.length-1) curentResult[title] = []
64
+ else curentResult[title] = {}
65
+ }
66
+ curentResult = curentResult[title]
67
+ if(previousTitles[index] !== title) {
68
+ const indentation = ' '.repeat(index);
69
+ console.log(bold+cblue+'%s'+reset,`${indentation} ${title}`);
70
+ previousTitles[index] = title;
71
+ }
72
+ });
73
+ SimpleTest.curentResult = curentResult
74
+ try {
75
+ if (test.before) await test.before();
76
+ await test.test();
77
+ if (test.after) await test.after();
78
+ } catch (error) {
79
+ if(showFullError) console.log(error)
80
+ else console.log(`${SimpleTest.space}${cred}Error:${reset} ${error.message}`);
81
+ curentResult.push({error})
82
+ }
83
+ console.log('\n');
84
+ }
85
+ if(typeof afterAllCallback == 'function') await afterAllCallback()
86
+ }
87
+
88
+ static assert(value,testTitle) {
89
+ if(testTitle) SimpleTest.testTitle = testTitle
90
+ SimpleTest.logResult(value)
91
+ }
92
+
93
+ static expect(value1) {
94
+ let { logResult, isEqual } = SimpleTest
95
+ const methods = {
96
+ is: (text='') => {
97
+ SimpleTest.testTitle = text;
98
+ SimpleTest.not = false
99
+ return methods;
100
+ },
101
+ isNot: (text='') => {
102
+ SimpleTest.testTitle = text;
103
+ SimpleTest.not = true
104
+ return methods;
105
+ },
106
+ equalTo: (value2) => {logResult(value1 === value2);},
107
+ between(value2,value3) {logResult(value1 >= value2 && value1 <= value3);},
108
+ below(value2) {logResult(value1 < value2);},
109
+ above(value2) {logResult(value1 > value2);},
110
+ atLeast(value2) {logResult(value1 >= value2);},
111
+ atMost(value2) {logResult(value1 <= value2);},
112
+ sameAs: (value2) => {logResult(isEqual(value1, value2));},
113
+ defined: () => {logResult(value1 !== undefined);},
114
+ matchTo(pattern) {logResult(pattern.test(new RegExp(value1)));},
115
+ includes(value2) {logResult(value1.includes(value2));},
116
+ error: () => {
117
+ if(typeof value1 !== 'function') return console.log('Expected value has to be function')
118
+ try {
119
+ let result = value1()
120
+ if(result instanceof Error) logResult(true)
121
+ else logResult(false);
122
+ } catch (error) {logResult(true)}
123
+ },
124
+ hasProperty: (property) => {logResult(Object.prototype.hasOwnProperty.call(value1, property));},
125
+ instanceof: (classType) => {logResult(value1.constructor.name == classType.name)},
126
+ closeTo(value2, decimalPlaces = 2) {
127
+ const multiplier = 10 ** decimalPlaces;
128
+ const roundedActual = Math.round(value1 * multiplier);
129
+ const roundedExpected = Math.round(value2 * multiplier);
130
+ const diff = Math.abs(roundedActual - roundedExpected) / multiplier;
131
+ logResult(diff < 1 / multiplier);
132
+ }
133
+ };
134
+ return methods;
135
+ }
136
+
137
+ static logResult(result) {
138
+ let {curentResult,testTitle,not,space,colors} = SimpleTest
139
+ let {cgreen,cred,cgray,reset} = colors
140
+ if(not) result = !result
141
+ curentResult.push({result:result ? true : false,testTitle,error:null})
142
+ const status = result ? 'Success' : 'Failed';
143
+ const color = result ? cgreen : cred;
144
+ if (testTitle == '') console.log(`${color}${space}${status}${reset}`);
145
+ else console.log(`${space}${color}${status}: ${testTitle ? `${cgray}${testTitle}` : ''}`+reset,);
146
+ SimpleTest.not = false
147
+ SimpleTest.testTitle = ''
148
+ }
149
+
150
+ static isEqual(obj1, obj2, visited = new WeakMap()) {
151
+ if (obj1 === obj2) return true;
152
+ if (visited.has(obj1) && visited.get(obj1) === obj2) return true;
153
+ const type1 = typeof obj1;
154
+ const type2 = typeof obj2;
155
+
156
+ if (type1 !== type2 || type1 !== 'object' || obj1 === null || obj2 === null) return false;
157
+ visited.set(obj1, obj2);
158
+
159
+ const keys1 = Object.keys(obj1);
160
+ const keys2 = Object.keys(obj2);
161
+ if(keys1.length !== keys2.length) return false;
162
+
163
+ for (let key of keys1) {
164
+ if (!keys2.includes(key) || !SimpleTest.isEqual(obj1[key], obj2[key], visited)) return false;
165
+ }
166
+ return true;
167
+ };
168
+ }
169
+ try {module.exports = SimpleTest} catch (error) {}