als-layout 1.3.3 → 2.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.
- package/lib/build-root.js +4 -3
- package/lib/elements/image.js +1 -1
- package/lib/elements/keywords.js +5 -5
- package/lib/elements/link.js +1 -1
- package/lib/elements/script.js +2 -2
- package/lib/layout.js +42 -12
- package/lib/onload.js +9 -0
- package/lib/render/build-app.js +22 -0
- package/lib/render/component-hierarchy.js +28 -0
- package/package.json +4 -3
- package/readme.md +99 -23
- package/tests/build-app.test.js +80 -0
- package/tests/build-root.test.js +21 -1
- package/tests/component-hierarchy.test.js +51 -0
- package/tests/elements.test.js +317 -75
- package/tests/layout.test.js +210 -0
- package/tests/integrative.test.js +0 -33
package/lib/build-root.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
const { buildFromCache, Root, parseHTML, cacheDoc } = require('als-document')
|
|
1
|
+
const { buildFromCache, Root, parseHTML, cacheDoc, Document } = require('als-document')
|
|
2
2
|
|
|
3
3
|
function buildRoot(item) {
|
|
4
4
|
const root =
|
|
5
5
|
(item instanceof Root) ? buildFromCache(cacheDoc(item))
|
|
6
6
|
: (typeof item === 'string') ? parseHTML(item)
|
|
7
|
-
: (typeof item === 'object') ? buildFromCache(item)
|
|
7
|
+
: (typeof item === 'object' && item.tagName) ? buildFromCache(item)
|
|
8
8
|
: new Root()
|
|
9
9
|
|
|
10
10
|
const firstNode = root.childNodes[0]
|
|
11
|
-
if(!firstNode || firstNode.tagName !== '!DOCTYPE') root.insert(1,/*html*/`<!DOCTYPE html>`)
|
|
11
|
+
if(!firstNode || firstNode.tagName !== '!DOCTYPE') {root.insert(1,/*html*/`<!DOCTYPE html>`)}
|
|
12
|
+
|
|
12
13
|
return root
|
|
13
14
|
}
|
|
14
15
|
|
package/lib/elements/image.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const addMeta = require('./add-meta')
|
|
2
2
|
function addImage(image,version, layout) {
|
|
3
|
-
if (image && version) image +=
|
|
3
|
+
if (image && version) image += (image.includes('?') ? '&' : '?') + `v=${version}`
|
|
4
4
|
|
|
5
5
|
addMeta({property:'og:image',content:image},layout)
|
|
6
6
|
addMeta({name:'twitter:image',content:image},layout)
|
package/lib/elements/keywords.js
CHANGED
|
@@ -3,17 +3,17 @@ const { SingleNode } = require('als-document')
|
|
|
3
3
|
function keywords(keywords = [], layout) {
|
|
4
4
|
const { root } = layout
|
|
5
5
|
let keywordsElement = root.$('meta[name=keywords]')
|
|
6
|
-
if (!keywordsElement) {
|
|
7
|
-
keywordsElement = new SingleNode('meta', { name: 'keywords' })
|
|
8
|
-
layout.head.insert(2, keywordsElement)
|
|
9
|
-
}
|
|
6
|
+
if (!keywordsElement) keywordsElement = new SingleNode('meta', { name: 'keywords' })
|
|
10
7
|
const content = keywordsElement.getAttribute('content')
|
|
11
8
|
const existingKeywords = content ? content.split(',') : []
|
|
12
9
|
keywords.forEach(keyword => {
|
|
13
10
|
keyword = keyword.trim()
|
|
14
11
|
if (!existingKeywords.includes(keyword)) existingKeywords.push(keyword)
|
|
15
12
|
});
|
|
16
|
-
|
|
13
|
+
if(existingKeywords.length) {
|
|
14
|
+
keywordsElement.setAttribute('content', existingKeywords.join())
|
|
15
|
+
layout.head.insert(2, keywordsElement)
|
|
16
|
+
}
|
|
17
17
|
return layout
|
|
18
18
|
}
|
|
19
19
|
|
package/lib/elements/link.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
const { SingleNode } = require('als-document')
|
|
2
2
|
function addLink(href, version, layout) {
|
|
3
3
|
if(!href || typeof href !== 'string') return layout
|
|
4
|
+
if (href && version) href += (href.includes('?') ? '&' : '?') + `v=${version}`
|
|
4
5
|
const selector = `link[rel=stylesheet][href^="${href}"]`
|
|
5
6
|
let linkElement = layout.root.$(selector)
|
|
6
7
|
if (linkElement) return
|
|
7
|
-
if (href && version) href += href.includes('?') ? '&' : '?' + `v=${version}`
|
|
8
8
|
linkElement = new SingleNode('link', { rel: 'stylesheet', href })
|
|
9
9
|
layout.head.insert(2, linkElement)
|
|
10
10
|
return layout
|
package/lib/elements/script.js
CHANGED
|
@@ -4,12 +4,12 @@ function addScript(attrs = {}, innerHTML = '', head = true, version, layout) {
|
|
|
4
4
|
if(attrs.src) {
|
|
5
5
|
const selector = `script[src="${attrs.src}"]`
|
|
6
6
|
if (attrs.src && layout.root.$(selector)) return layout
|
|
7
|
-
if (attrs.src && version) attrs.src += attrs.src.includes('?') ? '&' : '?' + `v=${version}`
|
|
7
|
+
if (attrs.src && version) attrs.src += (attrs.src.includes('?') ? '&' : '?') + `v=${version}`
|
|
8
8
|
}
|
|
9
9
|
if(Object.keys(attrs).length || innerHTML) {
|
|
10
10
|
const script = new Node('script', attrs)
|
|
11
11
|
if(innerHTML) script.innerHTML = innerHTML
|
|
12
|
-
if (head) layout.
|
|
12
|
+
if (head) layout.head.insert(2, script)
|
|
13
13
|
else layout.body.insert(3, script)
|
|
14
14
|
}
|
|
15
15
|
return layout
|
package/lib/layout.js
CHANGED
|
@@ -3,47 +3,77 @@ const {
|
|
|
3
3
|
addKeywords, addStyle, addDescription, addTitle, addImage,
|
|
4
4
|
addUrl, addFavicon, addScript, addLink, charset, viewport
|
|
5
5
|
} = require('./elements/index')
|
|
6
|
+
const build$App = require('./render/build-app')
|
|
6
7
|
const buildRoot = require('./build-root')
|
|
7
|
-
|
|
8
|
+
const onload = require('./onload')
|
|
8
9
|
class Layout {
|
|
9
10
|
constructor(layout, options = {}) {
|
|
10
11
|
this.options = options
|
|
11
12
|
this.root = buildRoot(layout)
|
|
12
13
|
this.body; this.head;
|
|
14
|
+
this.components = {}
|
|
15
|
+
this.data = {}
|
|
16
|
+
this.actions = {}
|
|
17
|
+
this.utils = {}
|
|
13
18
|
}
|
|
14
19
|
|
|
20
|
+
onload() {this.script({},onload); return this}
|
|
21
|
+
|
|
15
22
|
get html() {
|
|
16
23
|
const lang = this.options.lang || 'en'
|
|
17
|
-
if (!this.root.$('html')) this.root.insert(2
|
|
24
|
+
if (!this.root.$('html')) this.root.insert(2, `<html></html>`)
|
|
18
25
|
const htmlElement = this.root.$('html')
|
|
19
|
-
if(lang && htmlElement.getAttribute('lang') !== lang) {
|
|
20
|
-
htmlElement.setAttribute('lang',lang)
|
|
26
|
+
if (lang && htmlElement.getAttribute('lang') !== lang) {
|
|
27
|
+
htmlElement.setAttribute('lang', lang)
|
|
21
28
|
}
|
|
22
29
|
return htmlElement
|
|
23
30
|
}
|
|
24
31
|
get body() {
|
|
25
|
-
if (!this.root
|
|
26
|
-
return this.root
|
|
32
|
+
if (!this.root.$('body')) this.html.insert(2, `<body></body>`)
|
|
33
|
+
return this.root.$('body')
|
|
27
34
|
}
|
|
28
35
|
get head() {
|
|
29
|
-
if (!this.root
|
|
30
|
-
return this.root
|
|
36
|
+
if (!this.root.$('head')) this.html.insert(1, `<head></head>`)
|
|
37
|
+
return this.root.$('head')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
update(element, done = []) {
|
|
41
|
+
for (const componentName in this.components) {
|
|
42
|
+
const fn = this.components[componentName]
|
|
43
|
+
let components = element.getAttribute('component') === componentName
|
|
44
|
+
? [element]
|
|
45
|
+
: [...element.querySelectorAll(`[component=${componentName}]`)]
|
|
46
|
+
components = components.filter(el => !done.includes(el))
|
|
47
|
+
components.forEach((element, i) => {
|
|
48
|
+
element.componentIndex = i
|
|
49
|
+
fn(element, this)
|
|
50
|
+
done.push(element)
|
|
51
|
+
this.update(element, done)
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
render() {
|
|
57
|
+
const done = []
|
|
58
|
+
this.update(this.root, done)
|
|
59
|
+
this.script({}, build$App(done,this),false)
|
|
60
|
+
return this.rawHtml
|
|
31
61
|
}
|
|
32
62
|
|
|
33
63
|
get rawHtml() { return this.root.innerHTML }
|
|
34
64
|
get cached() { return cacheDoc(this.root) }
|
|
35
65
|
get clone() { return new Layout(this.root, this.options) }
|
|
36
66
|
|
|
37
|
-
version(v) {this.v = v; return this;}
|
|
67
|
+
version(v) { this.v = v; return this; }
|
|
38
68
|
keywords(keywords = []) { return addKeywords(keywords, this) }
|
|
39
69
|
description(description) { return addDescription(description, this) }
|
|
40
70
|
title(title) { return addTitle(title, this) }
|
|
41
71
|
style(styles, minified) { return addStyle(styles, minified, this) }
|
|
42
|
-
image(image,v=this.v) { return addImage(image, v, this) }
|
|
72
|
+
image(image, v = this.v) { return addImage(image, v, this) }
|
|
43
73
|
url(url, host = this.options.host) { return addUrl(url, host, this) }
|
|
44
74
|
favicon(href) { return addFavicon(href, this) }
|
|
45
|
-
script(attributes, innerHTML, head = true, v=this.v) { return addScript(attributes, innerHTML, head, v, this) }
|
|
46
|
-
link(href, v=this.v) { return addLink(href, v, this) }
|
|
75
|
+
script(attributes, innerHTML, head = true, v = this.v) { return addScript(attributes, innerHTML, head, v, this) }
|
|
76
|
+
link(href, v = this.v) { return addLink(href, v, this) }
|
|
47
77
|
charset(newCharset = 'UTF-8') { return charset(newCharset, this) }
|
|
48
78
|
viewport(newViewport = 'width=device-width, initial-scale=1.0') { return viewport(newViewport, this) }
|
|
49
79
|
}
|
package/lib/onload.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
module.exports = `document.addEventListener('DOMContentLoaded', function() {
|
|
2
|
+
const elements = document.querySelectorAll('[onload]');
|
|
3
|
+
elements.forEach(element => {
|
|
4
|
+
const onloadCode = element.getAttribute('onload');
|
|
5
|
+
const func = Function('"use strict"; return function() { ' + onloadCode + ' }');
|
|
6
|
+
func().call(element);
|
|
7
|
+
element.removeAttribute('onload');
|
|
8
|
+
});
|
|
9
|
+
});`
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const componentHierarchy = require('./component-hierarchy')
|
|
2
|
+
module.exports = function(done=[],layout) {
|
|
3
|
+
let strComponents = 'components:{}', updateFn = 'update:()=>{}'
|
|
4
|
+
if(done.length) {
|
|
5
|
+
strComponents = 'components:{'+componentHierarchy(done).map(componentName => {
|
|
6
|
+
return `"${componentName}":${layout.components[componentName].toString()}`
|
|
7
|
+
}).join(',')+'}'
|
|
8
|
+
updateFn = `update:function ${layout.update.toString()}`
|
|
9
|
+
}
|
|
10
|
+
const strData = 'data:'+JSON.stringify(layout.data)
|
|
11
|
+
const $ = 'function $(selector,parent = document) {return parent.querySelector(selector)}'
|
|
12
|
+
const $$ = 'function $$(selector,parent = document) {return [...parent.querySelectorAll(selector)]}'
|
|
13
|
+
let actions = 'actions:{'
|
|
14
|
+
for(const actionName in layout.actions) {
|
|
15
|
+
actions += `${actionName}:${layout.actions[actionName].toString()},`
|
|
16
|
+
}
|
|
17
|
+
let utils = 'utils:{'
|
|
18
|
+
for(const utilsName in layout.utils) {
|
|
19
|
+
utils += `${utilsName}:${layout.utils[utilsName].toString()},`
|
|
20
|
+
}
|
|
21
|
+
return `window.$App = {browser:true,${strData},${strComponents},${actions}},${utils}},${updateFn},$:${$},$$:${$$}}`
|
|
22
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
function componentHierarchy(elements) {
|
|
2
|
+
|
|
3
|
+
const entries = elements
|
|
4
|
+
.map(el => ([el, el.getAttribute('component'), el.ancestors]))
|
|
5
|
+
.sort((a, b) => a[2].length - b[2].length)
|
|
6
|
+
|
|
7
|
+
const result = []
|
|
8
|
+
while(entries.length > 0) {
|
|
9
|
+
const [element, componentName, ancestors] = entries.shift()
|
|
10
|
+
if(element.getAttribute('part') !== null) {
|
|
11
|
+
element.removeAttribute('component')
|
|
12
|
+
element.removeAttribute('part')
|
|
13
|
+
continue
|
|
14
|
+
}
|
|
15
|
+
const partsInside = element.$$('[component][part]')
|
|
16
|
+
partsInside.forEach(element => { element.removeAttribute('part') });
|
|
17
|
+
entries.forEach(entry => {
|
|
18
|
+
if(entry[1] !== componentName) return
|
|
19
|
+
if(entry[2].includes(element)) {
|
|
20
|
+
throw new Error(`The component '${componentName}' acts as both a parent and a child, which may complicate rendering.`);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
if(!result.includes(componentName)) result.push(componentName)
|
|
24
|
+
}
|
|
25
|
+
return result
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = componentHierarchy
|
package/package.json
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "als-layout",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Html layout constructor",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"directories": {
|
|
7
7
|
"lib": "lib"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"test": "node --test --experimental-test-coverage"
|
|
10
|
+
"test": "node --test --experimental-test-coverage",
|
|
11
|
+
"report": "node --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info"
|
|
11
12
|
},
|
|
12
13
|
"keywords": [],
|
|
13
14
|
"author": "Alex Sorkin",
|
|
14
15
|
"license": "ISC",
|
|
15
16
|
"dependencies": {
|
|
16
17
|
"als-css-parser": "^0.5.0",
|
|
17
|
-
"als-document": "^1.1
|
|
18
|
+
"als-document": "^1.2.1",
|
|
18
19
|
"als-simple-css": "^9.1.0"
|
|
19
20
|
}
|
|
20
21
|
}
|
package/readme.md
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
# Als-layout
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`Als-layout` is an HTML layout constructor for Node.js that allows you to manage HTML elements, styles, and scripts through JavaScript. It's perfect for server-side HTML generation or dynamic page modifications before delivery to the client.
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
* styles as separated tags
|
|
7
|
-
* version method
|
|
8
|
-
* script,link,image - version parameter
|
|
9
|
-
* updated als-document version
|
|
5
|
+
## What's New in 2.1.0
|
|
10
6
|
|
|
11
|
-
|
|
7
|
+
* updated als-document version
|
|
8
|
+
* [part] attribute for static components
|
|
9
|
+
* onload() method
|
|
10
|
+
* bugs fixed
|
|
11
|
+
|
|
12
|
+
## Install
|
|
12
13
|
|
|
13
14
|
```bash
|
|
14
15
|
npm i als-layout
|
|
15
16
|
```
|
|
16
17
|
|
|
17
|
-
## Basic
|
|
18
|
+
## Basic Usage
|
|
18
19
|
|
|
19
20
|
```js
|
|
20
21
|
const Layout = require('als-layout')
|
|
@@ -23,30 +24,29 @@ const layout = new Layout()
|
|
|
23
24
|
.viewport() // default width=device-width, initial-scale=1.0
|
|
24
25
|
.title('Test title') // adding/updating title and meta[og:title]
|
|
25
26
|
.favicon('/favicon.png') // adding/updating link[rel=icon][type=image/x-icon] with new href
|
|
26
|
-
.keywords(['some','keyword']) // adding/updating meta[name=keywords]. not adding existing keywords
|
|
27
|
-
.image('/main-image.png','1.5') // adding/updating meta - og:image,twitter:image,twitter:card
|
|
28
|
-
.description('Cool site') // adding/updating meta og:description,twitter:description and description tag
|
|
29
|
-
.version('1.0.0') // adds version parameter to link, script.src and image
|
|
27
|
+
.keywords(['some', 'keyword']) // adding/updating meta[name=keywords]. not adding existing keywords
|
|
28
|
+
.image('/main-image.png', '1.5') // adding/updating meta - og:image, twitter:image, twitter:card
|
|
29
|
+
.description('Cool site') // adding/updating meta og:description, twitter:description, and description tag
|
|
30
|
+
.version('1.0.0') // adds version parameter to link, script.src, and image
|
|
30
31
|
.url('/some', 'http://site.com') // adding/updating meta[og:url] and link[rel="canonical"]
|
|
31
|
-
.style([{body:{m:0,bgc:'whitesmoke'}}]) // adding as simple-css styles to existing/new style tag
|
|
32
|
-
.style('body {margin:0;
|
|
33
|
-
.link('/styles.css','2.0') // adding link[rel=stylesheet] if such href not exists
|
|
34
|
-
.script({src:'/app.js'},'', true,'3.0') // set script with src to head if such src not exists
|
|
32
|
+
.style([{body:{m:0, bgc:'whitesmoke'}}]) // adding as simple-css styles to existing/new style tag
|
|
33
|
+
.style('body {margin:0; background-color:whitesmoke;}', true) // adding css styles to existing/new style tag. Second parameter is minified (default=false).
|
|
34
|
+
.link('/styles.css', '2.0') // adding link[rel=stylesheet] if such href not exists
|
|
35
|
+
.script({src:'/app.js'}, '', true, '3.0') // set script with src to head if such src not exists
|
|
35
36
|
.script({}, 'console.log("hello world")', false) // set script with script code to footer
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
// Accessors for document parts
|
|
38
39
|
layout.body // getter for body element (if not exists, created)
|
|
39
40
|
layout.head // getter for head element (if not exists, created)
|
|
40
41
|
layout.html // getter for html element (if not exists, created)
|
|
41
42
|
|
|
43
|
+
// Outputs
|
|
42
44
|
layout.rawHtml // raw html
|
|
43
45
|
layout.cached // cached DOM
|
|
44
|
-
layout.clone // new layout object clone for
|
|
46
|
+
layout.clone // new layout object clone for current object
|
|
45
47
|
```
|
|
46
48
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
## Advanced usage
|
|
49
|
+
## Advanced Usage
|
|
50
50
|
|
|
51
51
|
```js
|
|
52
52
|
const Layout = require('./lib/layout')
|
|
@@ -56,14 +56,90 @@ const options = {
|
|
|
56
56
|
host:'http://example.com', // host for url method
|
|
57
57
|
lang:'fr' // for <html lang="fr"></html>
|
|
58
58
|
}
|
|
59
|
-
const layout = new Layout(raw,options)
|
|
59
|
+
const layout = new Layout(raw, options)
|
|
60
60
|
console.log(layout.rawHtml)
|
|
61
61
|
// <!DOCTYPE html><html lang="fr"><head></head><body></body></html>
|
|
62
62
|
|
|
63
63
|
const homePage = layout.clone
|
|
64
64
|
homePage.title('Home page')
|
|
65
65
|
homePage.body.innerHTML = /*html*/`<h1>Home page</h1>`
|
|
66
|
-
console.log(homePage.rawHtml)
|
|
66
|
+
console.log(homePage.rawHtml)
|
|
67
67
|
// <!DOCTYPE html><html lang="fr"><head><title>Home page</title><meta property="og:title" content="Home page"></head><body><h1>Home page</h1></body></html>
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
+
## Rendering
|
|
71
|
+
|
|
72
|
+
Each layout instance has a render method and the following objects:
|
|
73
|
+
```js
|
|
74
|
+
layout.data
|
|
75
|
+
layout.components
|
|
76
|
+
layout.utils
|
|
77
|
+
layout.actions
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The render method returns raw HTML after building elements with the attribute component. It will create HTML raw which will include a `window.$App` object with the following:
|
|
81
|
+
|
|
82
|
+
```js
|
|
83
|
+
window.$App = {
|
|
84
|
+
data,
|
|
85
|
+
components,
|
|
86
|
+
utils,
|
|
87
|
+
actions,
|
|
88
|
+
$(selector, parent=document), // querySelector
|
|
89
|
+
$$(selector, parent=document), // querySelectorAll
|
|
90
|
+
update(element), // update element if it has component attribute
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
## Counter Example
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
const fs = require('fs')
|
|
99
|
+
const Layout = require('als-layout')
|
|
100
|
+
|
|
101
|
+
const layout = new Layout().charset().viewport().title('Counter')
|
|
102
|
+
layout.data.counter = 0
|
|
103
|
+
layout.components.counter = function(element, $App) {
|
|
104
|
+
element.innerHTML = `${$App.data.counter}`
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
layout.actions = {
|
|
108
|
+
increase: () => { $App.data.counter++; $App.update($App.$('[component=counter]')) },
|
|
109
|
+
decrease: () => { $App.data.counter--; $App.update($App.$('[component=counter]')) }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
layout.body.innerHTML = /*html*/`
|
|
113
|
+
<button onclick="$App.actions.increase()">Increase</button>
|
|
114
|
+
<span component="counter"></span>
|
|
115
|
+
<button onclick="$App.actions.decrease()">Decrease</button>
|
|
116
|
+
`
|
|
117
|
+
const time1 = performance.now()
|
|
118
|
+
const rawHtml = layout.render()
|
|
119
|
+
const time2 = performance.now()
|
|
120
|
+
console.log(`${time2 - time1}ms`) // e.g., 1.0649ms
|
|
121
|
+
|
|
122
|
+
fs.writeFileSync('counter.html', rawHtml, 'utf-8')
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Each component gets a `componentIndex` which is available inside the component function.
|
|
126
|
+
```js
|
|
127
|
+
element.componentIndex
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
### parts
|
|
132
|
+
|
|
133
|
+
By default render method adds all used components to `$App.components`.
|
|
134
|
+
By adding `part` attribute component will not be added, except cases, the component is part of another component.
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
## onload
|
|
138
|
+
|
|
139
|
+
By adding onload attribute, you can run scripts for each element after dom content has loaded.
|
|
140
|
+
|
|
141
|
+
Example how it works:
|
|
142
|
+
```js
|
|
143
|
+
const layout = new Layout().charset().viewport().title('On load').onload()
|
|
144
|
+
layout.body.innerHTML = /*html*/`<div onload="this.innerHTML = 'new content'">original content</div>`
|
|
145
|
+
```
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const { describe, it } = require('node:test');
|
|
3
|
+
const build$App = require('../lib/render/build-app');
|
|
4
|
+
|
|
5
|
+
describe('build-$App Functionality', () => {
|
|
6
|
+
|
|
7
|
+
it('should handle empty input arrays correctly', () => {
|
|
8
|
+
const done = [];
|
|
9
|
+
const layout = {
|
|
10
|
+
components: {},
|
|
11
|
+
};
|
|
12
|
+
const result = build$App(done, layout);
|
|
13
|
+
assert.strictEqual(result, 'window.$App = {browser:true,data:undefined,components:{},actions:{},utils:{},update:()=>{},$:function $(selector,parent = document) {return parent.querySelector(selector)},$$:function $$(selector,parent = document) {return [...parent.querySelectorAll(selector)]}}');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should process multiple components correctly', () => {
|
|
17
|
+
const called = []
|
|
18
|
+
const done = [
|
|
19
|
+
{
|
|
20
|
+
getAttribute: (attr) => attr === 'component' ? 'comp1' : null,
|
|
21
|
+
removeAttribute(att) {called.push(att)},
|
|
22
|
+
$$(){return []},
|
|
23
|
+
ancestors:2
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
getAttribute: (attr) => attr === 'component' ? 'comp2' : null,
|
|
27
|
+
removeAttribute(att) {called.push(att)},
|
|
28
|
+
$$(){return []},
|
|
29
|
+
ancestors:2
|
|
30
|
+
}
|
|
31
|
+
];
|
|
32
|
+
const layout = {
|
|
33
|
+
components: {
|
|
34
|
+
comp1: () => 'Component1',
|
|
35
|
+
comp2: () => 'Component2'
|
|
36
|
+
},
|
|
37
|
+
actions: {},
|
|
38
|
+
utils: {},
|
|
39
|
+
data: {},
|
|
40
|
+
update: function update() { }
|
|
41
|
+
};
|
|
42
|
+
const result = build$App(done, layout);
|
|
43
|
+
assert(result.includes(`"comp1":${layout.components.comp1.toString()}`))
|
|
44
|
+
assert(result.includes(`"comp2":${layout.components.comp2.toString()}`))
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should add action functions correctly', () => {
|
|
48
|
+
const done = [];
|
|
49
|
+
const layout = {
|
|
50
|
+
components: {},
|
|
51
|
+
actions: {
|
|
52
|
+
action1: () => console.log('Action 1'),
|
|
53
|
+
action2: () => console.log('Action 2')
|
|
54
|
+
},
|
|
55
|
+
utils: {},
|
|
56
|
+
data: {},
|
|
57
|
+
update: function update() { }
|
|
58
|
+
};
|
|
59
|
+
const result = build$App(done, layout);
|
|
60
|
+
assert(result.includes(`action1:${layout.actions.action1.toString()}`))
|
|
61
|
+
assert(result.includes(`action2:${layout.actions.action2.toString()}`))
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should integrate utilities correctly', () => {
|
|
65
|
+
const done = [];
|
|
66
|
+
const layout = {
|
|
67
|
+
components: {},
|
|
68
|
+
actions: {},
|
|
69
|
+
utils: {
|
|
70
|
+
util1: () => console.log('Utility 1'),
|
|
71
|
+
util2: () => console.log('Utility 2')
|
|
72
|
+
},
|
|
73
|
+
data: {},
|
|
74
|
+
update: function update() { }
|
|
75
|
+
};
|
|
76
|
+
const result = build$App(done, layout);
|
|
77
|
+
assert(result.includes(`util1:${layout.utils.util1.toString()}`))
|
|
78
|
+
assert(result.includes(`util2:${layout.utils.util2.toString()}`))
|
|
79
|
+
});
|
|
80
|
+
});
|
package/tests/build-root.test.js
CHANGED
|
@@ -22,4 +22,24 @@ describe('Basic tests', function () {
|
|
|
22
22
|
const root = buildRoot()
|
|
23
23
|
assert(root.childNodes[0].tagName === '!DOCTYPE')
|
|
24
24
|
});
|
|
25
|
-
|
|
25
|
+
|
|
26
|
+
it('should create an instance of Root from HTML string', () => {
|
|
27
|
+
const htmlString = '<html><head><title>Test</title></head><body></body></html>';
|
|
28
|
+
const root = buildRoot(htmlString);
|
|
29
|
+
assert(root instanceof Root);
|
|
30
|
+
assert.strictEqual(root.childNodes.length > 0, true);
|
|
31
|
+
assert.strictEqual(root.childNodes[0].tagName, '!DOCTYPE');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should create an instance of Root from object', () => {
|
|
35
|
+
const docObject = cacheDoc('<html></html>')
|
|
36
|
+
const root = buildRoot(docObject);
|
|
37
|
+
assert(root instanceof Root);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should create an empty Root if object does not have tagName', () => {
|
|
41
|
+
const root = buildRoot({});
|
|
42
|
+
assert(root instanceof Root);
|
|
43
|
+
});
|
|
44
|
+
})
|
|
45
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const { parseHTML } = require('als-document')
|
|
2
|
+
const assert = require('assert');
|
|
3
|
+
const { describe, it } = require('node:test')
|
|
4
|
+
const componentHierarchy = require('../lib/render/component-hierarchy')
|
|
5
|
+
|
|
6
|
+
function shuffleArray(array) {
|
|
7
|
+
for (let i = array.length - 1; i > 0; i--) {
|
|
8
|
+
const j = Math.floor(Math.random() * (i + 1)); // Получаем случайный индекс от 0 до i
|
|
9
|
+
[array[i], array[j]] = [array[j], array[i]]; // Обмен элементов местами
|
|
10
|
+
}
|
|
11
|
+
return array;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('Basic tests', () => {
|
|
15
|
+
it('should build correct hierarchy', () => {
|
|
16
|
+
const root = parseHTML(/*html*/`<div component="some">
|
|
17
|
+
<div>
|
|
18
|
+
<div component="some1"></div>
|
|
19
|
+
<div>
|
|
20
|
+
<div component="some2">
|
|
21
|
+
<div component="some3"></div>
|
|
22
|
+
</div>
|
|
23
|
+
<div component="some2"></div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
</div>
|
|
27
|
+
</div>`)
|
|
28
|
+
|
|
29
|
+
const result = componentHierarchy(shuffleArray(root.$$('[component]')))
|
|
30
|
+
assert.deepStrictEqual(result,[ 'some', 'some1', 'some2', 'some3' ])
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should build correct hierarchy', () => {
|
|
34
|
+
const root = parseHTML(/*html*/`<div component="some">
|
|
35
|
+
<div>
|
|
36
|
+
<div component="some1"></div>
|
|
37
|
+
<div>
|
|
38
|
+
<div component="some2">
|
|
39
|
+
<div component="some3"></div>
|
|
40
|
+
</div>
|
|
41
|
+
<div component="some2">
|
|
42
|
+
<div component="some"></div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
</div>
|
|
47
|
+
</div>`)
|
|
48
|
+
|
|
49
|
+
assert.throws(() => componentHierarchy(shuffleArray(root.$$('[component]'))))
|
|
50
|
+
})
|
|
51
|
+
})
|
package/tests/elements.test.js
CHANGED
|
@@ -2,31 +2,79 @@ const assert = require('assert');
|
|
|
2
2
|
const Layout = require('../lib/layout');
|
|
3
3
|
const { describe, it, beforeEach } = require('node:test')
|
|
4
4
|
const Simple = require('als-simple-css')
|
|
5
|
-
|
|
5
|
+
const { SingleNode } = require('als-document')
|
|
6
|
+
const keywords = require('../lib/elements/keywords')
|
|
7
|
+
|
|
8
|
+
describe('Charset tests', () => {
|
|
6
9
|
let layout;
|
|
7
10
|
|
|
8
11
|
beforeEach(() => layout = new Layout());
|
|
9
12
|
|
|
13
|
+
it('should update charset if meta[charset] already exists', () => {
|
|
14
|
+
layout.head.insert(1, new SingleNode('meta', { charset: 'old-charset' }));
|
|
15
|
+
const newCharset = 'new-charset';
|
|
16
|
+
layout.charset(newCharset);
|
|
17
|
+
assert.strictEqual(layout.root.$('meta[charset]').getAttribute('charset'), newCharset, 'Charset should be updated');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should insert charset at second position if not present', () => {
|
|
21
|
+
const charset = 'UTF-8';
|
|
22
|
+
layout.charset(charset);
|
|
23
|
+
assert.strictEqual(layout.head.childNodes[0].tagName, 'meta', 'Meta should be at second position');
|
|
24
|
+
assert.strictEqual(layout.head.childNodes[0].getAttribute('charset'), charset, 'Charset not set correctly');
|
|
25
|
+
});
|
|
26
|
+
|
|
10
27
|
it('should add charset correctly', () => {
|
|
11
28
|
const charset = 'test';
|
|
12
29
|
layout.charset(charset);
|
|
13
30
|
assert.strictEqual(layout.root.$('meta[charset]').getAttribute('charset'), charset, 'Charset not set correctly');
|
|
14
31
|
});
|
|
32
|
+
});
|
|
15
33
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
34
|
+
describe('Favicon tests', () => {
|
|
35
|
+
let layout;
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
layout = new Layout();
|
|
39
|
+
// layout.head.insert(1, new SingleNode('title', {})); // Добавляем элемент title для проверки позиции
|
|
22
40
|
});
|
|
23
41
|
|
|
24
|
-
it('should
|
|
42
|
+
it('should update favicon href if link[rel="icon"] already exists', () => {
|
|
43
|
+
const oldHref = 'old-favicon.ico';
|
|
44
|
+
layout.head.insert(2, new SingleNode('link', { rel: 'icon', href: oldHref, type: 'image/x-icon' }));
|
|
45
|
+
const newHref = 'new-favicon.ico';
|
|
46
|
+
layout.favicon(newHref);
|
|
47
|
+
assert.strictEqual(layout.root.$('link[rel="icon"]').getAttribute('href'), newHref, 'Favicon href should be updated');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should insert favicon at second position if not present', () => {
|
|
51
|
+
const faviconHref = 'favicon.ico';
|
|
52
|
+
layout.favicon(faviconHref);
|
|
53
|
+
assert.strictEqual(layout.head.childNodes[0].tagName, 'link', 'Favicon link should be at second position');
|
|
54
|
+
assert.strictEqual(layout.head.childNodes[0].getAttribute('href'), faviconHref, 'Favicon href not set correctly');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should add favicon correctly', () => {
|
|
25
58
|
const faviconHref = 'favicon.ico';
|
|
26
59
|
layout.favicon(faviconHref);
|
|
27
60
|
assert.strictEqual(layout.root.$('link[rel="icon"]').getAttribute('href'), faviconHref, 'Favicon not set correctly');
|
|
28
61
|
});
|
|
29
62
|
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('Image tests', () => {
|
|
66
|
+
let layout;
|
|
67
|
+
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
layout = new Layout();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should add twitter:card meta tag', () => {
|
|
73
|
+
const imageUrl = 'test-image.jpg';
|
|
74
|
+
layout.image(imageUrl);
|
|
75
|
+
assert.strictEqual(layout.root.$('meta[name="twitter:card"]').getAttribute('content'), 'summary_large_image', 'twitter:card not set correctly');
|
|
76
|
+
});
|
|
77
|
+
|
|
30
78
|
it('should add image correctly', () => {
|
|
31
79
|
const imageUrl = 'test-image.jpg';
|
|
32
80
|
layout.image(imageUrl);
|
|
@@ -34,30 +82,53 @@ describe('Elements Integration', () => {
|
|
|
34
82
|
assert.strictEqual(layout.root.$('meta[name="twitter:image"]').getAttribute('content'), imageUrl, 'Image not set correctly');
|
|
35
83
|
});
|
|
36
84
|
|
|
37
|
-
it('should
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
85
|
+
it('should handle cases where image URL already has query parameters', () => {
|
|
86
|
+
const imageUrl = 'test-image.jpg?existing=param';
|
|
87
|
+
const version = '456';
|
|
88
|
+
layout.image(imageUrl, version);
|
|
89
|
+
const expectedUrl = imageUrl + '&v=' + version;
|
|
90
|
+
const img = layout.root.$('meta[property="og:image"]').getAttribute('content')
|
|
91
|
+
assert(img === expectedUrl, 'Versioned image URL with existing parameters not set correctly');
|
|
41
92
|
});
|
|
42
93
|
|
|
43
|
-
it('should add
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
94
|
+
it('should add version parameter to image URL if version is provided', () => {
|
|
95
|
+
const imageUrl = 'test-image.jpg';
|
|
96
|
+
const version = '123';
|
|
97
|
+
layout.image(imageUrl, version);
|
|
98
|
+
const expectedUrl = imageUrl + '?v=' + version;
|
|
99
|
+
assert.strictEqual(layout.root.$('meta[property="og:image"]').getAttribute('content'), expectedUrl, 'Versioned image URL not set correctly in og:image');
|
|
100
|
+
assert.strictEqual(layout.root.$('meta[name="twitter:image"]').getAttribute('content'), expectedUrl, 'Versioned image URL not set correctly in twitter:image');
|
|
47
101
|
});
|
|
102
|
+
});
|
|
48
103
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
104
|
+
describe('Keywords tests', () => {
|
|
105
|
+
let layout;
|
|
106
|
+
|
|
107
|
+
beforeEach(() => {
|
|
108
|
+
layout = new Layout();
|
|
54
109
|
});
|
|
55
110
|
|
|
111
|
+
it('should add new keywords to an existing meta tag', () => {
|
|
112
|
+
layout.head.insert(2, new SingleNode('meta', { name: 'keywords', content: 'initial' }));
|
|
113
|
+
const additionalKeywords = ['keyword1', 'keyword2'];
|
|
114
|
+
keywords(additionalKeywords, layout);
|
|
115
|
+
const expectedContent = 'initial,keyword1,keyword2';
|
|
116
|
+
assert.strictEqual(layout.root.$('meta[name="keywords"]').getAttribute('content'), expectedContent, 'Existing keywords not updated correctly');
|
|
117
|
+
});
|
|
56
118
|
|
|
57
|
-
it('should add
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
119
|
+
it('should not add duplicate keywords', () => {
|
|
120
|
+
layout.head.insert(2, new SingleNode('meta', { name: 'keywords', content: 'keyword1,keyword2' }));
|
|
121
|
+
const additionalKeywords = ['keyword2', 'keyword3'];
|
|
122
|
+
keywords(additionalKeywords, layout);
|
|
123
|
+
const expectedContent = 'keyword1,keyword2,keyword3';
|
|
124
|
+
assert.strictEqual(layout.root.$('meta[name="keywords"]').getAttribute('content'), expectedContent, 'Duplicate keywords were added');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should handle keywords with leading or trailing spaces', () => {
|
|
128
|
+
const messyKeywords = [' keyword1', 'keyword2 '];
|
|
129
|
+
keywords(messyKeywords, layout);
|
|
130
|
+
const expectedContent = 'keyword1,keyword2';
|
|
131
|
+
assert.strictEqual(layout.root.$('meta[name="keywords"]').getAttribute('content'), expectedContent, 'Keywords with spaces not trimmed correctly');
|
|
61
132
|
});
|
|
62
133
|
|
|
63
134
|
it('should add keywords correctly', () => {
|
|
@@ -66,39 +137,91 @@ describe('Elements Integration', () => {
|
|
|
66
137
|
assert(layout.root.$('meta[name="keywords"]').getAttribute('content') === keywords.join(), 'Keywords not set correctly');
|
|
67
138
|
});
|
|
68
139
|
|
|
69
|
-
it('should
|
|
70
|
-
|
|
71
|
-
layout.
|
|
72
|
-
assert.strictEqual(layout.root.$('meta[name="viewport"]').getAttribute('content'), viewportContent, 'Viewport not set correctly');
|
|
140
|
+
it('should handle empty keywords array', () => {
|
|
141
|
+
keywords([], layout);
|
|
142
|
+
assert(!layout.root.$('meta[name="keywords"]'), 'Meta tag for empty keywords should not be created');
|
|
73
143
|
});
|
|
74
144
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('Link', () => {
|
|
148
|
+
let layout;
|
|
149
|
+
beforeEach(() => layout = new Layout());
|
|
150
|
+
|
|
151
|
+
it('should add a new link element without version', () => {
|
|
152
|
+
const href = 'style.css';
|
|
153
|
+
layout.link(href);
|
|
154
|
+
assert.strictEqual(layout.root.$('link[rel="stylesheet"]').getAttribute('href'), href, 'Link href should match the provided href');
|
|
79
155
|
});
|
|
80
156
|
|
|
81
|
-
it('
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
layout.
|
|
85
|
-
assert(layout.root.$('
|
|
86
|
-
})
|
|
157
|
+
it('should add a new link element with version', () => {
|
|
158
|
+
const href = 'style.css';
|
|
159
|
+
const version = '1.0';
|
|
160
|
+
layout.link(href, version);
|
|
161
|
+
assert.strictEqual(layout.root.$('link[rel="stylesheet"]').getAttribute('href'), `${href}?v=${version}`, 'Link href should include version query parameter');
|
|
162
|
+
});
|
|
87
163
|
|
|
88
|
-
it('should add
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
layout.
|
|
93
|
-
assert(layout.root.$$(
|
|
94
|
-
layout.style(styles2);
|
|
95
|
-
assert(layout.root.$$('style').length === 1)
|
|
96
|
-
const inner = layout.root.$('style').innerHTML
|
|
97
|
-
assert(inner.includes(styles1));
|
|
98
|
-
assert(inner.includes(styles2));
|
|
164
|
+
it('should not add a link if one with the same href and version already exists', () => {
|
|
165
|
+
const href = 'style.css';
|
|
166
|
+
const version = '1.0';
|
|
167
|
+
layout.link(href, version);
|
|
168
|
+
layout.link(href, version);
|
|
169
|
+
assert.strictEqual(layout.root.$$(`link[rel="stylesheet"][href="${href}?v=${version}"]`).length, 1, 'Should not add duplicate link with the same version');
|
|
99
170
|
});
|
|
100
171
|
|
|
101
|
-
|
|
172
|
+
it('should handle invalid href or version correctly', () => {
|
|
173
|
+
layout.link('', '1.0');
|
|
174
|
+
layout.link(null, '1.0');
|
|
175
|
+
// layout.link('style.css', '');
|
|
176
|
+
// layout.link('style.css', null);
|
|
177
|
+
assert.strictEqual(layout.root.$('link[rel="stylesheet"]'), null, 'Should not add a link when href or version are invalid');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should add link correctly', () => {
|
|
181
|
+
const href = 'style.css';
|
|
182
|
+
layout.link(href);
|
|
183
|
+
assert.strictEqual(layout.root.$('link[rel="stylesheet"]').getAttribute('href'), href, 'Link not set correctly');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should not add a new link element if one already exists with the same href and no version', () => {
|
|
187
|
+
const href = 'style.css';
|
|
188
|
+
layout.link(href); // Добавление ссылки без версии
|
|
189
|
+
layout.link(href); // Повторное добавление той же ссылки без версии
|
|
190
|
+
assert.strictEqual(layout.root.$$(`link[rel="stylesheet"][href="${href}"]`).length, 1, 'Should not add duplicate link without version');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should not add a link if href is undefined or null', () => {
|
|
194
|
+
layout.link(undefined, '1.0');
|
|
195
|
+
layout.link(null);
|
|
196
|
+
assert.strictEqual(layout.root.$('link[rel="stylesheet"]'), null, 'Should not add a link when href is undefined or null');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('should handle different versions for the same href', () => {
|
|
200
|
+
const href = 'style.css';
|
|
201
|
+
const version1 = '1.0';
|
|
202
|
+
const version2 = '1.1';
|
|
203
|
+
layout.link(href, version1);
|
|
204
|
+
layout.link(href, version2);
|
|
205
|
+
assert.strictEqual(layout.root.$$(`link[rel="stylesheet"]`).length, 2, 'Should add different links for different versions');
|
|
206
|
+
assert.strictEqual(layout.root.$$(`link[rel="stylesheet"][href="${href}?v=${version1}"]`).length, 1, 'First version link should exist');
|
|
207
|
+
assert.strictEqual(layout.root.$$(`link[rel="stylesheet"][href="${href}?v=${version2}"]`).length, 1, 'Second version link should exist');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should correctly add version when href already has parameters', () => {
|
|
211
|
+
const href = 'style.css?param=value';
|
|
212
|
+
const version = '1.0';
|
|
213
|
+
layout.link(href, version);
|
|
214
|
+
assert.strictEqual(layout.root.$('link[rel="stylesheet"]').getAttribute('href'), `${href}&v=${version}`, 'Href should include version appended with &');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should not add a link when one with a similar href prefix exists', () => {
|
|
218
|
+
const href = 'style.css';
|
|
219
|
+
layout.link(href);
|
|
220
|
+
layout.link(href + '?param=value', '1.0');
|
|
221
|
+
assert.strictEqual(layout.root.$$(`link[rel="stylesheet"]`).length, 2, 'Should recognize different full hrefs as different links');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
})
|
|
102
225
|
|
|
103
226
|
describe('Scripts', () => {
|
|
104
227
|
let layout;
|
|
@@ -121,6 +244,37 @@ describe('Scripts', () => {
|
|
|
121
244
|
assert.strictEqual(layout.root.$('script').innerHTML, '', 'Script innerHTML should be empty');
|
|
122
245
|
});
|
|
123
246
|
|
|
247
|
+
it('should add script correctly', () => {
|
|
248
|
+
const scriptContent = 'console.log("Hello, world!");';
|
|
249
|
+
layout.script({}, scriptContent);
|
|
250
|
+
assert.strictEqual(layout.root.$('script').innerHTML, scriptContent, 'Script content not set correctly');
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('should add version to script src', () => {
|
|
254
|
+
const src = 'script.js';
|
|
255
|
+
const version = '1.0';
|
|
256
|
+
layout.script({ src }, '', true, version);
|
|
257
|
+
assert.strictEqual(layout.root.$('script').getAttribute('src'), `${src}?v=${version}`, 'Script src should include version');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should not add script if no attributes and no innerHTML', () => {
|
|
261
|
+
layout.script({}, '');
|
|
262
|
+
assert.strictEqual(layout.root.$('script'), null, 'Script should not be added if there are no attributes and no innerHTML');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('should add script after body when head is false', () => {
|
|
266
|
+
const scriptContent = 'console.log("Script in body");';
|
|
267
|
+
layout.script({ src: 'scriptbody.js' }, scriptContent, false);
|
|
268
|
+
assert.strictEqual(layout.body.next.$('script').innerHTML, scriptContent, 'Script should be added to body');
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('should append version parameter correctly when src already has parameters', () => {
|
|
272
|
+
const srcWithParams = 'script.js?existing=param';
|
|
273
|
+
const version = '1.0';
|
|
274
|
+
layout.script({ src: srcWithParams }, '', true, version);
|
|
275
|
+
assert.strictEqual(layout.root.$('script').getAttribute('src'), `${srcWithParams}&v=${version}`, 'Version parameter should be appended with &');
|
|
276
|
+
});
|
|
277
|
+
|
|
124
278
|
})
|
|
125
279
|
|
|
126
280
|
describe('Styles', () => {
|
|
@@ -138,6 +292,32 @@ describe('Styles', () => {
|
|
|
138
292
|
console.log(layout.root.$('style').innerHTML)
|
|
139
293
|
assert.strictEqual(layout.root.$('style').innerHTML, 'body{color:blue}div{font-size:12px}', 'Styles should be minified');
|
|
140
294
|
});
|
|
295
|
+
|
|
296
|
+
it('should add style correctly', () => {
|
|
297
|
+
const styles = 'body { background-color: black; }';
|
|
298
|
+
layout.style(styles);
|
|
299
|
+
assert(layout.root.$('style').innerHTML.includes(styles), 'Styles not set correctly');
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('Should add simple styles', () => {
|
|
303
|
+
const styles = [{ body: { bgc: 'black' } }]
|
|
304
|
+
const css = new Simple(styles).stylesheet()
|
|
305
|
+
layout.style(styles);
|
|
306
|
+
assert(layout.root.$('style').innerHTML.includes(css), 'Styles not set correctly');
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
it('should add styles to existing style tag', () => {
|
|
310
|
+
assert(layout.root.$$('style').length === 0)
|
|
311
|
+
const styles1 = 'body { background-color: black; }';
|
|
312
|
+
const styles2 = 'body { margin: 0; }';
|
|
313
|
+
layout.style(styles1);
|
|
314
|
+
assert(layout.root.$$('style').length === 1)
|
|
315
|
+
layout.style(styles2);
|
|
316
|
+
assert(layout.root.$$('style').length === 1)
|
|
317
|
+
const inner = layout.root.$('style').innerHTML
|
|
318
|
+
assert(inner.includes(styles1));
|
|
319
|
+
assert(inner.includes(styles2));
|
|
320
|
+
});
|
|
141
321
|
})
|
|
142
322
|
|
|
143
323
|
describe('Url', () => {
|
|
@@ -149,39 +329,101 @@ describe('Url', () => {
|
|
|
149
329
|
assert.strictEqual(layout.root.$('link[rel="canonical"]'), null, 'Canonical URL should not be added for invalid URLs');
|
|
150
330
|
});
|
|
151
331
|
|
|
332
|
+
|
|
333
|
+
it('should add url correctly', () => {
|
|
334
|
+
const url = 'http://localhost';
|
|
335
|
+
layout.url(url);
|
|
336
|
+
assert(layout.root.$('link[rel="canonical"]').getAttribute('href') === url, 'Canonical URL not set correctly');
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('should add og:url meta correctly', () => {
|
|
340
|
+
const url = 'http://example.com';
|
|
341
|
+
const host = 'http://example.com';
|
|
342
|
+
layout.url(url, host);
|
|
343
|
+
assert.strictEqual(layout.root.$('meta[property="og:url"]').getAttribute('content'), url, 'og:url meta not set correctly');
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('should add canonical link if not already present', () => {
|
|
347
|
+
const url = 'http://example.com';
|
|
348
|
+
const host = 'http://example.com';
|
|
349
|
+
layout.url(url, host);
|
|
350
|
+
assert.strictEqual(layout.root.$('link[rel="canonical"]').getAttribute('href'), url, 'Canonical link should be added if not present');
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('should update existing canonical link', () => {
|
|
354
|
+
const initialUrl = 'http://example.com/initial';
|
|
355
|
+
const newUrl = 'http://example.com/new';
|
|
356
|
+
const host = 'http://example.com';
|
|
357
|
+
layout.url(initialUrl, host);
|
|
358
|
+
layout.url(newUrl, host);
|
|
359
|
+
assert.strictEqual(layout.root.$('link[rel="canonical"]').getAttribute('href'), newUrl, 'Canonical link should be updated with new URL');
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('should handle invalid URL with valid host', () => {
|
|
363
|
+
layout.url('http://', 'http://example.com');
|
|
364
|
+
assert.strictEqual(layout.root.$('meta[property="og:url"]'), null, 'Meta og:url should not be added for invalid URL');
|
|
365
|
+
assert.strictEqual(layout.root.$('link[rel="canonical"]'), null, 'Canonical link should not be added for invalid URL');
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
|
|
152
369
|
})
|
|
153
370
|
|
|
154
|
-
describe('
|
|
371
|
+
describe('layout.viewport',() => {
|
|
155
372
|
let layout;
|
|
373
|
+
|
|
156
374
|
beforeEach(() => layout = new Layout());
|
|
157
375
|
|
|
158
|
-
it('should
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
376
|
+
it('should update existing viewport meta correctly', () => {
|
|
377
|
+
// Добавляем изначальный viewport
|
|
378
|
+
const initialContent = 'width=device-width, initial-scale=1.0';
|
|
379
|
+
layout.viewport(initialContent);
|
|
380
|
+
|
|
381
|
+
// Обновляем viewport
|
|
382
|
+
const updatedContent = 'width=device-width, initial-scale=2.0';
|
|
383
|
+
layout.viewport(updatedContent);
|
|
384
|
+
|
|
385
|
+
// Проверяем, что содержимое meta обновлено
|
|
386
|
+
assert.strictEqual(layout.root.$('meta[name="viewport"]').getAttribute('content'), updatedContent, 'Existing viewport meta content should be updated');
|
|
162
387
|
});
|
|
163
|
-
|
|
164
|
-
it('should add a
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
layout.
|
|
168
|
-
|
|
388
|
+
|
|
389
|
+
it('should not add a second viewport meta if one already exists', () => {
|
|
390
|
+
// Добавляем изначальный viewport
|
|
391
|
+
const initialContent = 'width=device-width, initial-scale=1.0';
|
|
392
|
+
layout.viewport(initialContent);
|
|
393
|
+
|
|
394
|
+
// Пытаемся добавить ещё один
|
|
395
|
+
const secondContent = 'width=device-width, initial-scale=2.0';
|
|
396
|
+
layout.viewport(secondContent);
|
|
397
|
+
|
|
398
|
+
// Проверяем, что в документе только один элемент meta viewport
|
|
399
|
+
assert.strictEqual(layout.root.$$(`meta[name="viewport"]`).length, 1, 'Only one viewport meta should exist');
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('should add viewport correctly', () => {
|
|
403
|
+
const viewportContent = 'width=device-width, initial-scale=1.0';
|
|
404
|
+
layout.viewport(viewportContent);
|
|
405
|
+
assert.strictEqual(layout.root.$('meta[name="viewport"]').getAttribute('content'), viewportContent, 'Viewport not set correctly');
|
|
169
406
|
});
|
|
407
|
+
})
|
|
170
408
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
409
|
+
describe('description and title', () => {
|
|
410
|
+
let layout;
|
|
411
|
+
|
|
412
|
+
beforeEach(() => layout = new Layout());
|
|
413
|
+
|
|
414
|
+
it('should add description correctly', () => {
|
|
415
|
+
const description = 'Test Description';
|
|
416
|
+
layout.description(description);
|
|
417
|
+
assert.strictEqual(layout.root.$('meta[name="description"]').getAttribute('content'), description, 'Description not set correctly');
|
|
418
|
+
assert.strictEqual(layout.root.$('meta[property="og:description"]').getAttribute('content'), description, 'Description not set correctly');
|
|
419
|
+
assert.strictEqual(layout.root.$('meta[property="twitter:description"]').getAttribute('content'), description, 'Description not set correctly');
|
|
177
420
|
});
|
|
178
421
|
|
|
179
|
-
it('should
|
|
180
|
-
|
|
181
|
-
layout.
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
assert.strictEqual(layout.root.$('link[rel="stylesheet"]'), null, 'Should not add a link when href or version are invalid');
|
|
422
|
+
it('should add title correctly', () => {
|
|
423
|
+
const title = 'Test Title';
|
|
424
|
+
layout.title(title);
|
|
425
|
+
assert(layout.root.$('title').innerHTML === title, 'Title not set correctly');
|
|
426
|
+
assert(layout.root.$('[property="og:title"]').getAttribute('content') === title, 'Title not set correctly');
|
|
185
427
|
});
|
|
186
428
|
|
|
187
|
-
})
|
|
429
|
+
});
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const { describe, it, beforeEach } = require('node:test')
|
|
3
|
+
const Layout = require('../lib/layout');
|
|
4
|
+
const { buildFromCache } = require('als-document')
|
|
5
|
+
|
|
6
|
+
describe('Layout Integrative tests', () => {
|
|
7
|
+
let layout;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => layout = new Layout());
|
|
10
|
+
|
|
11
|
+
it('should integrate multiple elements correctly', () => {
|
|
12
|
+
layout.title('Test Title');
|
|
13
|
+
layout.description('Test Description');
|
|
14
|
+
layout.keywords(['keyword1', 'keyword2']);
|
|
15
|
+
|
|
16
|
+
assert(layout.root.$('title').innerHTML === 'Test Title', 'Title not integrated correctly');
|
|
17
|
+
assert(layout.root.$('meta[name="description"]').getAttribute('content') === 'Test Description', 'Description not integrated correctly');
|
|
18
|
+
assert(layout.root.$('meta[name="keywords"]').getAttribute('content') === 'keyword1,keyword2', 'Keywords not integrated correctly');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should update elements correctly when added multiple times', () => {
|
|
22
|
+
layout.title('First Title');
|
|
23
|
+
assert(layout.root.$('title').innerHTML === 'First Title', 'Title did not update correctly when added multiple times');
|
|
24
|
+
layout.title('Second Title');
|
|
25
|
+
assert(layout.root.$('title').innerHTML === 'Second Title', 'Title did not update correctly when added multiple times');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should remove elements correctly', () => {
|
|
29
|
+
layout.title('Test Title');
|
|
30
|
+
layout.root.$('title').remove();
|
|
31
|
+
assert(layout.root.$('title') === null, 'Title element was not removed correctly');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('onload', () => {
|
|
35
|
+
layout.onload()
|
|
36
|
+
const script = layout.root.$('script')
|
|
37
|
+
assert(script.innerHTML.includes(`document.addEventListener('DOMContentLoaded'`))
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('HTML Structure Initialization', () => {
|
|
43
|
+
let layout;
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
layout = new Layout();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should initialize html element correctly', () => {
|
|
50
|
+
assert.strictEqual(layout.html.tagName, 'html', 'HTML element should be initialized');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should not recreate html element if it already exists', () => {
|
|
54
|
+
const existingHtml = layout.html; // вызовем один раз
|
|
55
|
+
assert.strictEqual(layout.html, existingHtml, 'HTML element should not be recreated');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should initialize body element correctly', () => {
|
|
59
|
+
assert.strictEqual(layout.body.tagName, 'body', 'Body element should be initialized');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should not recreate body element if it already exists', () => {
|
|
63
|
+
const existingBody = layout.body; // вызовем один раз
|
|
64
|
+
assert.strictEqual(layout.body, existingBody, 'Body element should not be recreated');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should initialize head element correctly', () => {
|
|
68
|
+
assert.strictEqual(layout.head.tagName, 'head', 'Head element should be initialized');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should not recreate head element if it already exists', () => {
|
|
72
|
+
const existingHead = layout.head; // вызовем один раз
|
|
73
|
+
assert.strictEqual(layout.head, existingHead, 'Head element should not be recreated');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('Component Updating', () => {
|
|
78
|
+
let layout;
|
|
79
|
+
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
layout = new Layout();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should correctly update components', () => {
|
|
85
|
+
layout.components.test2 = (element, $App) => {
|
|
86
|
+
assert(element.componentIndex === 0)
|
|
87
|
+
element.innerHTML = 'test2 success'
|
|
88
|
+
}
|
|
89
|
+
layout.components.test1 = (element, $App) => {
|
|
90
|
+
assert(element.componentIndex === 0)
|
|
91
|
+
element.insert(2, 'test1 success')
|
|
92
|
+
}
|
|
93
|
+
layout.body.innerHTML = /*html*/`<div component="test1">
|
|
94
|
+
<div component="test2">failed</div>
|
|
95
|
+
</div>`
|
|
96
|
+
|
|
97
|
+
const done = []
|
|
98
|
+
layout.update(layout.root, done)
|
|
99
|
+
assert(done.length === 2)
|
|
100
|
+
assert(layout.rawHtml.includes('test1 success'))
|
|
101
|
+
assert(layout.rawHtml.includes('test2 success'))
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('should correctly render components', () => {
|
|
105
|
+
layout.components.test2 = (element, $App) => {
|
|
106
|
+
assert(element.componentIndex === 0)
|
|
107
|
+
element.innerHTML = 'test2 success'
|
|
108
|
+
}
|
|
109
|
+
layout.components.test1 = (element, $App) => {
|
|
110
|
+
assert(element.componentIndex === 0)
|
|
111
|
+
element.insert(2, 'test1 success')
|
|
112
|
+
}
|
|
113
|
+
layout.data.test = 'hello'
|
|
114
|
+
layout.body.innerHTML = /*html*/`<div component="test1">
|
|
115
|
+
<div component="test2">failed</div>
|
|
116
|
+
</div>`
|
|
117
|
+
|
|
118
|
+
const rawHtml = layout.render()
|
|
119
|
+
assert(rawHtml.includes('test1 success'))
|
|
120
|
+
assert(rawHtml.includes('test2 success'))
|
|
121
|
+
|
|
122
|
+
const script = rawHtml.match(/<[^>]*?script.*?>[^<]*<\/[^>]*?script.*?>/g)[0]
|
|
123
|
+
.replace('<script>window.$App = ', '')
|
|
124
|
+
.replace('</script>', '')
|
|
125
|
+
const fn = new Function(`return ${script}`)
|
|
126
|
+
const { browser, data, components, actions, utils, updata, $, $$ } = fn()
|
|
127
|
+
assert(browser === true)
|
|
128
|
+
assert.deepStrictEqual(Object.keys(components), ['test1', 'test2'])
|
|
129
|
+
assert(components.test1.toString() === layout.components.test1.toString())
|
|
130
|
+
assert(data.test === 'hello')
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('should add part to $App if it is component`s descendant', () => {
|
|
134
|
+
layout.body.innerHTML = /*html*/`
|
|
135
|
+
<div component="parent">
|
|
136
|
+
<div component="child" part></div>
|
|
137
|
+
</div>`;
|
|
138
|
+
layout.components.parent = (element, $App) => {};
|
|
139
|
+
layout.components.child = (element, $App) => {};
|
|
140
|
+
|
|
141
|
+
const rawHtml = layout.render();
|
|
142
|
+
const script = rawHtml.match(/<[^>]*?script.*?>[^<]*<\/[^>]*?script.*?>/g)[0]
|
|
143
|
+
.replace('<script>window.$App = ', '')
|
|
144
|
+
.replace('</script>', '')
|
|
145
|
+
const fn = new Function(`return ${script}`)
|
|
146
|
+
|
|
147
|
+
const {components} = fn()
|
|
148
|
+
assert(components.parent !== undefined)
|
|
149
|
+
assert(components.child !== undefined)
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should not add part to $App if it has no component ancestor', () => {
|
|
153
|
+
layout.body.innerHTML = /*html*/`
|
|
154
|
+
<div component="comp1"></div>
|
|
155
|
+
<div component="comp2" part></div>
|
|
156
|
+
`;
|
|
157
|
+
layout.components.comp1 = (element, $App) => {};
|
|
158
|
+
layout.components.comp2 = (element, $App) => {};
|
|
159
|
+
|
|
160
|
+
const rawHtml = layout.render();
|
|
161
|
+
const script = rawHtml.match(/<[^>]*?script.*?>[^<]*<\/[^>]*?script.*?>/g)[0]
|
|
162
|
+
.replace('<script>window.$App = ', '')
|
|
163
|
+
.replace('</script>', '')
|
|
164
|
+
const fn = new Function(`return ${script}`)
|
|
165
|
+
|
|
166
|
+
const {components} = fn()
|
|
167
|
+
assert(components.comp1 !== undefined)
|
|
168
|
+
assert(components.comp2 === undefined)
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('If no components, should add empty update function to $App', () => {
|
|
172
|
+
const rawHtml = layout.render();
|
|
173
|
+
assert( rawHtml.includes('update:()=>{}'));
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe('Cache and Clone Testing', () => {
|
|
179
|
+
let layout;
|
|
180
|
+
beforeEach(() => {
|
|
181
|
+
layout = new Layout();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should retrieve cached version of the layout', () => {
|
|
185
|
+
const cachedVersion = layout.cached;
|
|
186
|
+
assert.doesNotThrow(() => JSON.stringify(cachedVersion)) // should be object without recursions
|
|
187
|
+
assert(buildFromCache(cachedVersion).innerHTML === layout.rawHtml)
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should clone the layout correctly', () => {
|
|
191
|
+
const clone = layout.clone;
|
|
192
|
+
assert(clone instanceof Layout, 'Clone should be an instance of Layout');
|
|
193
|
+
assert.notStrictEqual(clone, layout, 'Clone should not be the same instance as the original');
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe('Version method tests', () => {
|
|
198
|
+
let layout;
|
|
199
|
+
|
|
200
|
+
beforeEach(() => {
|
|
201
|
+
layout = new Layout();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should set the version correctly', () => {
|
|
205
|
+
const version = '1.0.0';
|
|
206
|
+
layout.version(version);
|
|
207
|
+
assert.strictEqual(layout.v, version, 'Version should be set correctly');
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
const assert = require('assert');
|
|
2
|
-
const { describe, it, beforeEach } = require('node:test')
|
|
3
|
-
const Layout = require('../lib/layout');
|
|
4
|
-
|
|
5
|
-
describe('Layout Integrative tests', () => {
|
|
6
|
-
let layout;
|
|
7
|
-
|
|
8
|
-
beforeEach(() => layout = new Layout());
|
|
9
|
-
|
|
10
|
-
it('should integrate multiple elements correctly', () => {
|
|
11
|
-
layout.title('Test Title');
|
|
12
|
-
layout.description('Test Description');
|
|
13
|
-
layout.keywords(['keyword1', 'keyword2']);
|
|
14
|
-
|
|
15
|
-
assert(layout.root.$('title').innerHTML === 'Test Title', 'Title not integrated correctly');
|
|
16
|
-
assert(layout.root.$('meta[name="description"]').getAttribute('content') === 'Test Description', 'Description not integrated correctly');
|
|
17
|
-
assert(layout.root.$('meta[name="keywords"]').getAttribute('content') === 'keyword1,keyword2', 'Keywords not integrated correctly');
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('should update elements correctly when added multiple times', () => {
|
|
21
|
-
layout.title('First Title');
|
|
22
|
-
assert(layout.root.$('title').innerHTML === 'First Title', 'Title did not update correctly when added multiple times');
|
|
23
|
-
layout.title('Second Title');
|
|
24
|
-
assert(layout.root.$('title').innerHTML === 'Second Title', 'Title did not update correctly when added multiple times');
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('should remove elements correctly', () => {
|
|
28
|
-
layout.title('Test Title');
|
|
29
|
-
layout.root.$('title').remove();
|
|
30
|
-
assert(layout.root.$('title') === null, 'Title element was not removed correctly');
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
});
|