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');