diff --git a/packages/happy-dom/src/nodes/document/Document.ts b/packages/happy-dom/src/nodes/document/Document.ts index 38a28276f..8311c096e 100644 --- a/packages/happy-dom/src/nodes/document/Document.ts +++ b/packages/happy-dom/src/nodes/document/Document.ts @@ -47,6 +47,7 @@ import ProcessingInstruction from '../processing-instruction/ProcessingInstructi import ElementUtility from '../element/ElementUtility'; import HTMLCollection from '../element/HTMLCollection'; import VisibilityStateEnum from './VisibilityStateEnum'; +import NodeTypeEnum from '../node/NodeTypeEnum'; const PROCESSING_INSTRUCTION_TARGET_REGEXP = /^[a-z][a-z0-9-]+$/; @@ -678,7 +679,7 @@ export default class Document extends Node implements IDocument { for (const node of root.childNodes) { if (node['tagName'] === 'HTML') { documentElement = node; - } else if (node.nodeType === Node.DOCUMENT_TYPE_NODE) { + } else if (node.nodeType === NodeTypeEnum.documentTypeNode) { documentTypeNode = node; } @@ -704,10 +705,11 @@ export default class Document extends Node implements IDocument { } } + // Remaining nodes outside the element are added to the element. const body = ParentNodeUtility.getElementByTagName(this, 'body'); if (body) { for (const child of root.childNodes.slice()) { - if (child['tagName'] !== 'HTML' && child.nodeType !== Node.DOCUMENT_TYPE_NODE) { + if (child['tagName'] !== 'HTML' && child.nodeType !== NodeTypeEnum.documentTypeNode) { body.appendChild(child); } } diff --git a/packages/happy-dom/src/nodes/element/Element.ts b/packages/happy-dom/src/nodes/element/Element.ts index 61d8445e0..758a5b148 100644 --- a/packages/happy-dom/src/nodes/element/Element.ts +++ b/packages/happy-dom/src/nodes/element/Element.ts @@ -92,7 +92,7 @@ export default class Element extends Node implements IElement { public _attributes: { [k: string]: IAttr } = {}; private _classList: DOMTokenList = null; - public _isValue?: string; + public _isValue?: string | null = null; public _computedStyle: CSSStyleDeclaration | null = null; /** diff --git a/packages/happy-dom/src/nodes/html-unknown-element/HTMLUnknownElement.ts b/packages/happy-dom/src/nodes/html-unknown-element/HTMLUnknownElement.ts index 2aadaa03f..5493e8571 100644 --- a/packages/happy-dom/src/nodes/html-unknown-element/HTMLUnknownElement.ts +++ b/packages/happy-dom/src/nodes/html-unknown-element/HTMLUnknownElement.ts @@ -1,6 +1,11 @@ import HTMLElement from '../html-element/HTMLElement'; import INode from '../node/INode'; import IHTMLElement from '../html-element/IHTMLElement'; +import INodeList from '../node/INodeList'; +import IHTMLCollection from '../element/IHTMLCollection'; +import IElement from '../element/IElement'; +import NodeList from '../node/NodeList'; +import HTMLCollection from '../element/HTMLCollection'; /** * HTML Unknown Element. @@ -27,9 +32,48 @@ export default class HTMLUnknownElement extends HTMLElement implements IHTMLElem if (parentNode && !this._customElementDefineCallback) { const callback = (): void => { if (this.parentNode) { - const newElement = this.ownerDocument.createElement(tagName); - this.parentNode.insertBefore(newElement, this); - this.parentNode.removeChild(this); + const newElement = this.ownerDocument.createElement(tagName); + (>newElement.childNodes) = this.childNodes; + (>newElement.children) = this.children; + (newElement.isConnected) = this.isConnected; + + newElement._rootNode = this._rootNode; + newElement._formNode = this._formNode; + newElement._selectNode = this._selectNode; + newElement._textAreaNode = this._textAreaNode; + newElement._observers = this._observers; + newElement._isValue = this._isValue; + newElement._attributes = this._attributes; + + (>this.childNodes) = new NodeList(); + (>this.children) = new HTMLCollection(); + this._rootNode = null; + this._formNode = null; + this._selectNode = null; + this._textAreaNode = null; + this._observers = []; + this._isValue = null; + this._attributes = {}; + + for (let i = 0, max = this.parentNode.childNodes.length; i < max; i++) { + if (this.parentNode.childNodes[i] === this) { + this.parentNode.childNodes[i] = newElement; + break; + } + } + + if ((this.parentNode).children) { + for (let i = 0, max = (this.parentNode).children.length; i < max; i++) { + if ((this.parentNode).children[i] === this) { + (this.parentNode).children[i] = newElement; + break; + } + } + } + + if (newElement.isConnected && newElement.connectedCallback) { + newElement.connectedCallback(); + } } }; callbacks[tagName] = callbacks[tagName] || []; diff --git a/packages/happy-dom/src/nodes/node/Node.ts b/packages/happy-dom/src/nodes/node/Node.ts index 62e7f6adf..f1709e21e 100644 --- a/packages/happy-dom/src/nodes/node/Node.ts +++ b/packages/happy-dom/src/nodes/node/Node.ts @@ -406,12 +406,15 @@ export default class Node extends EventTarget implements INode { if (this.isConnected !== isConnected) { (this.isConnected) = isConnected; - if (isConnected && this.connectedCallback) { - this.connectedCallback(); - } else if (!isConnected && this.disconnectedCallback) { + if (!isConnected) { if (this.ownerDocument['_activeElement'] === this) { this.ownerDocument['_activeElement'] = null; } + } + + if (isConnected && this.connectedCallback) { + this.connectedCallback(); + } else if (!isConnected && this.disconnectedCallback) { this.disconnectedCallback(); } diff --git a/packages/happy-dom/test/CustomElement.ts b/packages/happy-dom/test/CustomElement.ts index f4c5b7b70..5f3628c01 100644 --- a/packages/happy-dom/test/CustomElement.ts +++ b/packages/happy-dom/test/CustomElement.ts @@ -48,16 +48,22 @@ export default class CustomElement extends new Window().HTMLElement { span { color: pink; } - .class1 { + .propKey { color: yellow; }
- + key1 is "${this.getAttribute('key1')}" and key2 is "${this.getAttribute( 'key2' )}". + ${this.childNodes + .map( + (child) => + '#' + child['nodeType'] + (child['tagName'] || '') + child.textContent + ) + .join(', ')}
`; diff --git a/packages/happy-dom/test/nodes/html-unknown-element/HTMLUnknownElement.test.ts b/packages/happy-dom/test/nodes/html-unknown-element/HTMLUnknownElement.test.ts index 1842b7bb9..9ae83ee4e 100644 --- a/packages/happy-dom/test/nodes/html-unknown-element/HTMLUnknownElement.test.ts +++ b/packages/happy-dom/test/nodes/html-unknown-element/HTMLUnknownElement.test.ts @@ -32,12 +32,53 @@ describe('HTMLUnknownElement', () => { expect(parent.children.length).toBe(1); - expect(parent.children[0] !== element).toBe(true); + expect(parent.children[0] instanceof CustomElement).toBe(true); expect(parent.children[0].shadowRoot.children.length).toBe(0); document.body.appendChild(parent); expect(parent.children[0].shadowRoot.children.length).toBe(2); }); + + it('Copies all properties from the unknown element to the new instance.', () => { + const element = document.createElement('custom-element'); + const child1 = document.createElement('div'); + const child2 = document.createElement('div'); + + element.appendChild(child1); + element.appendChild(child2); + + document.body.appendChild(element); + + const childNodes = element.childNodes; + const children = element.children; + const rootNode = (element._rootNode = document.createElement('div')); + const formNode = (element._formNode = document.createElement('div')); + const selectNode = (element._selectNode = document.createElement('div')); + const textAreaNode = (element._textAreaNode = document.createElement('div')); + const observers = element._observers; + const isValue = (element._isValue = 'test'); + const attributes = element._attributes; + + window.customElements.define('custom-element', CustomElement); + + const customElement = document.body.children[0]; + + expect(document.body.children.length).toBe(1); + expect(customElement instanceof CustomElement).toBe(true); + + expect(customElement.isConnected).toBe(true); + expect(customElement.shadowRoot.children.length).toBe(2); + + expect(customElement.childNodes === childNodes).toBe(true); + expect(customElement.children === children).toBe(true); + expect(customElement._rootNode === rootNode).toBe(true); + expect(customElement._formNode === formNode).toBe(true); + expect(customElement._selectNode === selectNode).toBe(true); + expect(customElement._textAreaNode === textAreaNode).toBe(true); + expect(customElement._observers === observers).toBe(true); + expect(customElement._isValue === isValue).toBe(true); + expect(customElement._attributes === attributes).toBe(true); + }); }); }); diff --git a/packages/happy-dom/test/xml-serializer/XMLSerializer.test.ts b/packages/happy-dom/test/xml-serializer/XMLSerializer.test.ts index 7eaedcd44..71329e220 100644 --- a/packages/happy-dom/test/xml-serializer/XMLSerializer.test.ts +++ b/packages/happy-dom/test/xml-serializer/XMLSerializer.test.ts @@ -166,14 +166,17 @@ describe('XMLSerializer', () => { span { color: pink; } - .class1 { + .propKey { color: yellow; }
- + key1 is "value1" and key2 is "value2". + + #1SPANSlottedcontent +
@@ -183,6 +186,67 @@ describe('XMLSerializer', () => { ); }); + it('Renders the code from the documentation for server-side rendering as expected.', () => { + document.write(` + + + Test page + + +
+ + Slotted content + +
+ + + + `); + + expect( + document.body + .querySelector('div') + .getInnerHTML({ includeShadowRoots: true }) + .replace(/\s/gm, '') + ).toBe( + ` + + Slotted content + + + `.replace(/\s/gm, '') + ); + }); + it('Does not escape unicode attributes.', () => { const div = document.createElement('div');