Repository: capricorn86/happy-dom
Pull request #108 (2020-10-12 19:46:34)

#106@minor: Adds support for parsing doctype


Patch:
diff --git a/packages/happy-dom/src/dom-implementation/DOMImplementation.ts b/packages/happy-dom/src/dom-implementation/DOMImplementation.ts
index 13ae243b..15aa045f 100644
--- a/packages/happy-dom/src/dom-implementation/DOMImplementation.ts
+++ b/packages/happy-dom/src/dom-implementation/DOMImplementation.ts
@@ -1,3 +1,4 @@
+import DocumentType from '../nodes/basic/document-type/DocumentType';
 import Document from '../nodes/basic/document/Document';
 import Window from '../window/Window';
 
@@ -22,4 +23,24 @@ export default class DOMImplementation {
 	public createHTMLDocument(): Document {
 		return new Document(this._window);
 	}
+
+	/**
+	 * Creates and returns an HTML Document.
+	 *
+	 * @param qualifiedName Qualified name.
+	 * @param publicId Public ID.
+	 * @param systemId System ID.
+	 */
+	public createDocumentType(
+		qualifiedName: string,
+		publicId: string,
+		systemId: string
+	): DocumentType {
+		DocumentType.ownerDocument = this._window.document;
+		const documentType = new DocumentType();
+		documentType.name = qualifiedName;
+		documentType.publicId = publicId;
+		documentType.systemId = systemId;
+		return documentType;
+	}
 }
diff --git a/packages/happy-dom/src/dom-parser/DOMParser.ts b/packages/happy-dom/src/dom-parser/DOMParser.ts
index 9b84e740..d6a934a6 100644
--- a/packages/happy-dom/src/dom-parser/DOMParser.ts
+++ b/packages/happy-dom/src/dom-parser/DOMParser.ts
@@ -1,5 +1,6 @@
 import Document from '../nodes/basic/document/Document';
-import HTMLParser from '../html-parser/HTMLParser';
+import XMLParser from '../xml-parser/XMLParser';
+import Node from '../nodes/basic/node/Node';
 
 /**
  * HTML parser.
@@ -26,12 +27,29 @@ export default class DOMParser {
 		const ownerDocument = (<typeof DOMParser>(<unknown>this.constructor)).ownerDocument;
 		const newDocument = new Document(ownerDocument.defaultView);
 
-		const root = HTMLParser.parse(newDocument, string);
-		const documentElement = root.querySelector('html');
+		newDocument.childNodes = [];
+		newDocument.children = [];
 
-		newDocument.documentElement.parentNode.removeChild(newDocument.documentElement);
+		const root = XMLParser.parse(newDocument, string);
+		let documentElement = null;
+		let documentTypeNode = null;
+
+		for (const node of root.childNodes) {
+			if (node['tagName'] === 'HTML') {
+				documentElement = node;
+			} else if (node.nodeType === Node.DOCUMENT_TYPE_NODE) {
+				documentTypeNode = node;
+			}
+
+			if (documentElement && documentTypeNode) {
+				break;
+			}
+		}
 
 		if (documentElement) {
+			if (documentTypeNode) {
+				newDocument.appendChild(documentTypeNode);
+			}
 			newDocument.appendChild(documentElement);
 			const body = newDocument.querySelector('body');
 			if (body) {
@@ -44,8 +62,8 @@ export default class DOMParser {
 			const bodyElement = newDocument.createElement('body');
 			const headElement = newDocument.createElement('head');
 
-			for (const child of root.childNodes.slice()) {
-				bodyElement.appendChild(child);
+			for (const node of root.childNodes.slice()) {
+				bodyElement.appendChild(node);
 			}
 
 			documentElement.appendChild(headElement);
diff --git a/packages/happy-dom/src/html-renderer/HTMLRenderer.ts b/packages/happy-dom/src/html-renderer/HTMLRenderer.ts
deleted file mode 100644
index 85e0d5f3..00000000
--- a/packages/happy-dom/src/html-renderer/HTMLRenderer.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import Element from '../nodes/basic/element/Element';
-import ShadowRoot from '../nodes/basic/shadow-root/ShadowRoot';
-import HTMLTemplateElement from '../nodes/elements/template/HTMLTemplateElement';
-import SelfClosingHTMLElements from '../html-config/SelfClosingHTMLElements';
-import UnclosedHTMLElements from '../html-config/UnclosedHTMLElements';
-import CommentNode from '../nodes/basic/comment-node/CommentNode';
-import { encode } from 'he';
-
-/**
- * Utility for converting an element to string.
- *
- * @class QuerySelector
- */
-export default class HTMLRenderer {
-	/**
-	 * Renders an element as HTML.
-	 *
-	 * @param element Element to render.
-	 * @return Result.
-	 */
-	public static getOuterHTML(element: Element): string {
-		const tagName = element.tagName.toLowerCase();
-
-		if (UnclosedHTMLElements.includes(tagName)) {
-			return `<${tagName}${this.getAttributes(element)}>`;
-		} else if (SelfClosingHTMLElements.includes(tagName)) {
-			return `<${tagName}${this.getAttributes(element)}/>`;
-		}
-
-		return `<${tagName}${this.getAttributes(element)}>${element.innerHTML}</${tagName}>`;
-	}
-
-	/**
-	 * Renders an element as HTML.
-	 *
-	 * @param element Element to render.
-	 * @returns Result.
-	 */
-	public static getInnerHTML(element: Element | ShadowRoot): string {
-		const renderElement = (<HTMLTemplateElement>element).content || element;
-		let html = '';
-
-		for (const child of renderElement.childNodes.slice()) {
-			if (child instanceof Element) {
-				html += child.outerHTML;
-			} else if (child instanceof CommentNode) {
-				html += '<!--' + child._textContent + '-->';
-			} else if (child['_textContent']) {
-				html += child['_textContent'];
-			}
-		}
-
-		return html;
-	}
-
-	/**
-	 * Returns attributes as a string.
-	 *
-	 * @param element Element.
-	 * @return Attributes.
-	 */
-	private static getAttributes(element: Element): string {
-		const attributes = [];
-		for (const name of Object.keys(element._attributes)) {
-			if (element._attributes[name] && element._attributes[name].value !== null) {
-				attributes.push(name + '="' + encode(element._attributes[name].value) + '"');
-			}
-		}
-		return attributes.length > 0 ? ' ' + attributes.join(' ') : '';
-	}
-}
diff --git a/packages/happy-dom/src/index.ts b/packages/happy-dom/src/index.ts
index 9d166633..7ed35b28 100644
--- a/packages/happy-dom/src/index.ts
+++ b/packages/happy-dom/src/index.ts
@@ -13,9 +13,11 @@ import ShadowRoot from './nodes/basic/shadow-root/ShadowRoot';
 import Node from './nodes/basic/node/Node';
 import TextNode from './nodes/basic/text-node/TextNode';
 import CommentNode from './nodes/basic/comment-node/CommentNode';
+import DocumentType from './nodes/basic/document-type/DocumentType';
 import TreeWalker from './tree-walker/TreeWalker';
 import CustomElementRegistry from './custom-element/CustomElementRegistry';
-import HTMLRenderer from './html-renderer/HTMLRenderer';
+import XMLParser from './xml-parser/XMLParser';
+import XMLSerializer from './xml-serializer/XMLSerializer';
 import HTMLElementTag from './html-config/HTMLElementTag';
 import SelfClosingHTMLElements from './html-config/SelfClosingHTMLElements';
 import UnclosedHTMLElements from './html-config/UnclosedHTMLElements';
@@ -36,9 +38,11 @@ export {
 	Node,
 	TextNode,
 	CommentNode,
+	DocumentType,
 	TreeWalker,
 	CustomElementRegistry,
-	HTMLRenderer,
+	XMLParser,
+	XMLSerializer,
 	HTMLElementTag,
 	SelfClosingHTMLElements,
 	UnclosedHTMLElements
diff --git a/packages/happy-dom/src/nodes/basic/document-type/DocumentType.ts b/packages/happy-dom/src/nodes/basic/document-type/DocumentType.ts
new file mode 100644
index 00000000..cd5a4822
--- /dev/null
+++ b/packages/happy-dom/src/nodes/basic/document-type/DocumentType.ts
@@ -0,0 +1,45 @@
+import Node from '../node/Node';
+
+/**
+ * DocumentType.
+ */
+export default class DocumentType extends Node {
+	public readonly nodeType = Node.DOCUMENT_TYPE_NODE;
+	public name: string = null;
+	public publicId = '';
+	public systemId = '';
+	public readonly textContent = null;
+
+	/**
+	 * Node name.
+	 *
+	 * @return Node name.
+	 */
+	public get nodeName(): string {
+		return this.name;
+	}
+
+	/**
+	 * Converts to string.
+	 *
+	 * @return String.
+	 */
+	public toString(): string {
+		return '[object DocumentType]';
+	}
+
+	/**
+	 * Clones a node.
+	 *
+	 * @override
+	 * @param [deep=false] "true" to clone deep.
+	 * @return Cloned node.
+	 */
+	public cloneNode(deep = false): DocumentType {
+		const clone = <DocumentType>super.cloneNode(deep);
+		clone.name = this.name;
+		clone.publicId = this.publicId;
+		clone.systemId = this.systemId;
+		return clone;
+	}
+}
diff --git a/packages/happy-dom/src/nodes/basic/document/Document.ts b/packages/happy-dom/src/nodes/basic/document/Document.ts
index 6261f8e0..a76a5684 100644
--- a/packages/happy-dom/src/nodes/basic/document/Document.ts
+++ b/packages/happy-dom/src/nodes/basic/document/Document.ts
@@ -6,13 +6,14 @@ import Window from '../../../window/Window';
 import Node from '../node/Node';
 import TreeWalker from '../../../tree-walker/TreeWalker';
 import DocumentFragment from '../document-fragment/DocumentFragment';
-import HTMLParser from '../../../html-parser/HTMLParser';
+import XMLParser from '../../../xml-parser/XMLParser';
 import Event from '../../../event/Event';
 import DOMImplementation from '../../../dom-implementation/DOMImplementation';
 import HTMLElementTag from '../../../html-config/HTMLElementTag';
 import INodeFilter from '../../../tree-walker/INodeFilter';
 import Attr from '../../../attribute/Attr';
 import NamespaceURI from '../../../html-config/NamespaceURI';
+import DocumentType from '../document-type/DocumentType';
 
 /**
  * Document.
@@ -36,10 +37,12 @@ export default class Document extends DocumentFragment {
 		this.defaultView = window;
 		this.implementation = new DOMImplementation(window);
 
+		const doctype = this.implementation.createDocumentType('html', '', '');
 		const documentElement = this.createElement('html');
 		const bodyElement = this.createElement('body');
 		const headElement = this.createElement('head');
 
+		this.appendChild(doctype);
 		this.appendChild(documentElement);
 
 		documentElement.appendChild(headElement);
@@ -64,6 +67,20 @@ export default class Document extends DocumentFragment {
 		return <HTMLElement>this.querySelector('html');
 	}
 
+	/**
+	 * Returns document type element.
+	 *
+	 * @return Document type.
+	 */
+	public get doctype(): DocumentType {
+		for (const node of this.childNodes) {
+			if (node instanceof DocumentType) {
+				return node;
+			}
+		}
+		return null;
+	}
+
 	/**
 	 * Returns <body> element.
 	 *
@@ -88,7 +105,7 @@ export default class Document extends DocumentFragment {
 	 * @param html HTML.
 	 */
 	public write(html: string): void {
-		const root = HTMLParser.parse(this, html);
+		const root = XMLParser.parse(this, html);
 
 		if (this._isFirstWrite || this._isFirstWriteAfterOpen) {
 			if (this._isFirstWrite) {
@@ -100,15 +117,44 @@ export default class Document extends DocumentFragment {
 			}
 
 			this._isFirstWriteAfterOpen = false;
+			let documentElement = null;
+			let documentTypeNode = null;
+
+			for (const node of root.childNodes) {
+				if (node['tagName'] === 'HTML') {
+					documentElement = node;
+				} else if (node.nodeType === Node.DOCUMENT_TYPE_NODE) {
+					documentTypeNode = node;
+				}
 
-			const documentElement = root.querySelector('html');
+				if (documentElement && documentTypeNode) {
+					break;
+				}
+			}
 
 			if (documentElement) {
-				this.appendChild(documentElement);
+				if (!this.documentElement) {
+					if (documentTypeNode) {
+						this.appendChild(documentTypeNode);
+					}
+
+					this.appendChild(documentElement);
+				} else {
+					const rootBody = root.querySelector('body');
+					const body = this.querySelector('body');
+					if (rootBody && body) {
+						for (const child of rootBody.childNodes.slice()) {
+							body.appendChild(child);
+						}
+					}
+				}
+
 				const body = this.querySelector('body');
 				if (body) {
 					for (const child of root.childNodes.slice()) {
-						body.appendChild(child);
+						if (child['tagName'] !== 'HTML' && child.nodeType !== Node.DOCUMENT_TYPE_NODE) {
+							body.appendChild(child);
+						}
 					}
 				}
 			} else {
diff --git a/packages/happy-dom/src/nodes/basic/element/Element.ts b/packages/happy-dom/src/nodes/basic/element/Element.ts
index 4bfeb23b..f0812254 100644
--- a/packages/happy-dom/src/nodes/basic/element/Element.ts
+++ b/packages/happy-dom/src/nodes/basic/element/Element.ts
@@ -4,13 +4,13 @@ import ShadowRoot from '../shadow-root/ShadowRoot';
 import Attr from '../../../attribute/Attr';
 import DOMRect from './DOMRect';
 import Range from './Range';
-import HTMLParser from '../../../html-parser/HTMLParser';
 import ClassList from './ClassList';
 import QuerySelector from '../../../query-selector/QuerySelector';
-import HTMLRenderer from '../../../html-renderer/HTMLRenderer';
 import MutationRecord from '../../../mutation-observer/MutationRecord';
 import MutationTypeConstant from '../../../mutation-observer/MutationType';
 import NamespaceURI from '../../../html-config/NamespaceURI';
+import XMLParser from '../../../xml-parser/XMLParser';
+import XMLSerializer from '../../../xml-serializer/XMLSerializer';
 
 /**
  * Element.
@@ -124,7 +124,12 @@ export default class Element extends Node {
 	 * @return HTML.
 	 */
 	public get innerHTML(): string {
-		return HTMLRenderer.getInnerHTML(this);
+		const xmlSerializer = new XMLSerializer();
+		let xml = '';
+		for (const node of this.childNodes) {
+			xml += xmlSerializer.serializeToString(node);
+		}
+		return xml;
 	}
 
 	/**
@@ -137,7 +142,7 @@ export default class Element extends Node {
 			this.removeChild(child);
 		}
 
-		for (const node of HTMLParser.parse(this.ownerDocument, html).childNodes.slice()) {
+		for (const node of XMLParser.parse(this.ownerDocument, html).childNodes.slice()) {
 			this.appendChild(node);
 		}
 	}
@@ -148,7 +153,7 @@ export default class Element extends Node {
 	 * @return HTML.
 	 */
 	public get outerHTML(): string {
-		return HTMLRenderer.getOuterHTML(this);
+		return new XMLSerializer().serializeToString(this);
 	}
 
 	/**
diff --git a/packages/happy-dom/src/nodes/basic/node/Node.ts b/packages/happy-dom/src/nodes/basic/node/Node.ts
index 27f836bb..4dc60c66 100644
--- a/packages/happy-dom/src/nodes/basic/node/Node.ts
+++ b/packages/happy-dom/src/nodes/basic/node/Node.ts
@@ -4,7 +4,7 @@ import MutationRecord from '../../../mutation-observer/MutationRecord';
 import MutationTypeConstant from '../../../mutation-observer/MutationType';
 import MutationObserverListener from '../../../mutation-observer/MutationListener';
 import Event from '../../../event/Event';
-import HTMLParser from '../../../html-parser/HTMLParser';
+import XMLParser from '../../../xml-parser/XMLParser';
 
 /**
  * Node
@@ -21,7 +21,7 @@ export default class Node extends EventTarget {
 	public ownerDocument: Document = null;
 	public parentNode: Node = null;
 	public readonly nodeType: number;
-	public readonly childNodes: Node[] = [];
+	public childNodes: Node[] = [];
 
 	// Protected properties
 	protected _isConnected = false;
@@ -401,11 +401,7 @@ export default class Node extends EventTarget {
 			const node = nodes[i];
 
 			if (typeof node === 'string') {
-				parent.childNodes.splice(
-					index,
-					0,
-					...HTMLParser.parse(this.ownerDocument, node).childNodes
-				);
+				parent.childNodes.splice(index, 0, ...XMLParser.parse(this.ownerDocument, node).childNodes);
 			} else {
 				parent.childNodes.splice(index, 0, node);
 			}
diff --git a/packages/happy-dom/src/nodes/basic/shadow-root/ShadowRoot.ts b/packages/happy-dom/src/nodes/basic/shadow-root/ShadowRoot.ts
index 624ccab3..831c98f6 100644
--- a/packages/happy-dom/src/nodes/basic/shadow-root/ShadowRoot.ts
+++ b/packages/happy-dom/src/nodes/basic/shadow-root/ShadowRoot.ts
@@ -1,14 +1,13 @@
 import DocumentFragment from '../document-fragment/DocumentFragment';
-import HTMLParser from '../../../html-parser/HTMLParser';
-import HTMLRenderer from '../../../html-renderer/HTMLRenderer';
 import Node from '../node/Node';
+import XMLParser from '../../../xml-parser/XMLParser';
+import XMLSerializer from '../../../xml-serializer/XMLSerializer';
 
 /**
  * ShadowRoot.
  */
 export default class ShadowRoot extends DocumentFragment {
 	public mode = 'open';
-	public _renderer: HTMLRenderer = null;
 
 	/**
 	 * Returns inner HTML.
@@ -16,20 +15,25 @@ export default class ShadowRoot extends DocumentFragment {
 	 * @return HTML.
 	 */
 	public get innerHTML(): string {
-		return HTMLRenderer.getInnerHTML(this);
+		const xmlSerializer = new XMLSerializer();
+		let xml = '';
+		for (const node of this.childNodes) {
+			xml += xmlSerializer.serializeToString(node);
+		}
+		return xml;
 	}
 
 	/**
 	 * Sets inner HTML.
 	 *
 	 * @param html HTML.
-	 * @return HTML.
 	 */
 	public set innerHTML(html: string) {
 		for (const child of this.childNodes.slice()) {
 			this.removeChild(child);
 		}
-		for (const node of HTMLParser.parse(this.ownerDocument, html).childNodes.slice()) {
+
+		for (const node of XMLParser.parse(this.ownerDocument, html).childNodes.slice()) {
 			this.appendChild(node);
 		}
 	}
diff --git a/packages/happy-dom/src/window/Window.ts b/packages/happy-dom/src/window/Window.ts
index 454ab079..4b3b3b46 100644
--- a/packages/happy-dom/src/window/Window.ts
+++ b/packages/happy-dom/src/window/Window.ts
@@ -18,6 +18,7 @@ import EventTypes from '../event/EventTypes.json';
 import MutationObserver from '../mutation-observer/MutationObserver';
 import HTMLElementClass from '../html-config/HTMLElementClass';
 import DOMParser from '../dom-parser/DOMParser';
+import XMLSerializer from '../xml-serializer/XMLSerializer';
 
 /**
  * Handles the Window.
@@ -42,6 +43,7 @@ export default class Window extends EventTarget implements NodeJS.Global {
 	public CustomElementRegistry = CustomElementRegistry;
 	public Window = Window;
 	public Headers = Map;
+	public XMLSerializer = XMLSerializer;
 
 	// Public Properties
 	public document: Document;
diff --git a/packages/happy-dom/src/html-parser/HTMLParser.ts b/packages/happy-dom/src/xml-parser/XMLParser.ts
similarity index 80%
rename from packages/happy-dom/src/html-parser/HTMLParser.ts
rename to packages/happy-dom/src/xml-parser/XMLParser.ts
index a19dddaf..de80c767 100755
--- a/packages/happy-dom/src/html-parser/HTMLParser.ts
+++ b/packages/happy-dom/src/xml-parser/XMLParser.ts
@@ -7,18 +7,21 @@ import { decode } from 'he';
 import NamespaceURI from '../html-config/NamespaceURI';
 
 const MARKUP_REGEXP = /<(\/?)([a-z][-.0-9_a-z]*)\s*([^>]*?)(\/?)>/gi;
-const ATTRIBUTE_REGEXP = /([^\s=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|(\S+)))/gi;
+const COMMENT_REGEXP = /<!--(.*?)-->/gi;
+const DOCUMENT_TYPE_REGEXP = /<!DOCTYPE[\s]([a-zA-Z]+)([^>]*)>/gm;
+const DOCUMENT_TYPE_ATTRIBUTE_REGEXP = /"([^"]+)"/gm;
+const ATTRIBUTE_REGEXP = /([^\s=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|(\S+)))/gms;
 const XMLNS_ATTRIBUTE_REGEXP = /xmlns[ ]*=[ ]*"([^"]+)"/;
 
 /**
- * HTML parser.
+ * XML parser.
  */
-export default class HTMLParser {
+export default class XMLParser {
 	/**
-	 * Parses HTML and returns a root element.
+	 * Parses XML/HTML and returns a root element.
 	 *
-	 * @param  {Document} document Document.
-	 * @param  data HTML data.
+	 * @param {Document} document Document.
+	 * @param data HTML data.
 	 * @return Root element.
 	 */
 	public static parse(document: Document, data: string): Element {
@@ -30,7 +33,9 @@ export default class HTMLParser {
 		let lastTextIndex = 0;
 		let match: RegExpExecArray;
 
-		while ((match = markupRegexp.exec(data))) {
+		data = data.trim();
+
+		while ((match = markupRegexp.exec(data.trim()))) {
 			const tagName = match[2].toLowerCase();
 			const isStartTag = !match[1];
 
@@ -120,11 +125,13 @@ export default class HTMLParser {
 	 */
 	private static getTextAndCommentNodes(document: Document, text: string): Node[] {
 		const nodes = [];
-		const commentRegexp = /<!--(.*?)-->/gms;
+		const commentRegExp = new RegExp(COMMENT_REGEXP, 'gms');
+		const documentTypeRegExp = new RegExp(DOCUMENT_TYPE_REGEXP, 'gm');
+		let hasDocumentType = false;
 		let lastIndex = 0;
 		let match;
 
-		while ((match = commentRegexp.exec(text))) {
+		while ((match = commentRegExp.exec(text))) {
 			if (match.index > 0) {
 				const textNode = document.createTextNode(text.substring(lastIndex, match.index));
 				nodes.push(textNode);
@@ -134,7 +141,33 @@ export default class HTMLParser {
 			lastIndex = match.index + match[0].length;
 		}
 
-		if (lastIndex < text.length) {
+		while ((match = documentTypeRegExp.exec(text))) {
+			let publicId = '';
+			let systemId = '';
+
+			if (match[2]) {
+				const attributes = [];
+				const attributeRegExp = new RegExp(DOCUMENT_TYPE_ATTRIBUTE_REGEXP, 'gm');
+				const isPublic = match[2].includes('PUBLIC');
+				let attributeMatch;
+				while ((attributeMatch = attributeRegExp.exec(match[2]))) {
+					attributes.push(attributeMatch[1]);
+				}
+				publicId = isPublic ? attributes[0] || '' : '';
+				systemId = isPublic ? attributes[1] || '' : attributes[0] || '';
+			}
+
+			const documentTypeNode = document.implementation.createDocumentType(
+				match[1],
+				publicId,
+				systemId
+			);
+
+			nodes.push(documentTypeNode);
+			hasDocumentType = true;
+		}
+
+		if (!hasDocumentType && lastIndex < text.length) {
 			const textNode = document.createTextNode(text.substring(lastIndex));
 			nodes.push(textNode);
 		}
diff --git a/packages/happy-dom/src/xml-serializer/XMLSerializer.ts b/packages/happy-dom/src/xml-serializer/XMLSerializer.ts
new file mode 100644
index 00000000..c27c8d0a
--- /dev/null
+++ b/packages/happy-dom/src/xml-serializer/XMLSerializer.ts
@@ -0,0 +1,71 @@
+import Element from '../nodes/basic/element/Element';
+import Node from '../nodes/basic/node/Node';
+import SelfClosingHTMLElements from '../html-config/SelfClosingHTMLElements';
+import UnclosedHTMLElements from '../html-config/UnclosedHTMLElements';
+import CommentNode from '../nodes/basic/comment-node/CommentNode';
+import DocumentType from '../nodes/basic/document-type/DocumentType';
+import { encode } from 'he';
+import DocumentFragment from '../nodes/basic/document-fragment/DocumentFragment';
+
+/**
+ * Utility for converting an element to string.
+ *
+ * @class QuerySelector
+ */
+export default class XMLSerializer {
+	/**
+	 * Renders an element as HTML.
+	 *
+	 * @param element Element to render.
+	 * @return Result.
+	 */
+	public serializeToString(root: Node): string {
+		if (root instanceof Element) {
+			const tagName = root.tagName.toLowerCase();
+			if (UnclosedHTMLElements.includes(tagName)) {
+				return `<${tagName}${this._getAttributes(root)}>`;
+			} else if (SelfClosingHTMLElements.includes(tagName)) {
+				return `<${tagName}${this._getAttributes(root)}/>`;
+			}
+
+			let xml = '';
+			for (const node of root.childNodes) {
+				xml += this.serializeToString(node);
+			}
+
+			return `<${tagName}${this._getAttributes(root)}>${xml}</${tagName}>`;
+		} else if (root instanceof DocumentFragment) {
+			let xml = '';
+			for (const node of root.childNodes) {
+				xml += this.serializeToString(node);
+			}
+			return xml;
+		} else if (root instanceof CommentNode) {
+			return `<!--${root._textContent}-->`;
+		} else if (root instanceof DocumentType) {
+			const identifier = root.publicId ? ' PUBLIC' : root.systemId ? ' SYSTEM' : '';
+			const publicId = root.publicId ? ` "${root.publicId}"` : '';
+			const systemId = root.systemId ? ` "${root.systemId}"` : '';
+			return `<!DOCTYPE ${root.name}${identifier}${publicId}${systemId}>`;
+		} else if (root['_textContent']) {
+			return root['_textContent'];
+		}
+		return '';
+	}
+
+	/**
+	 * Returns attributes as a string.
+	 *
+	 * @param element Element.
+	 * @return Attributes.
+	 */
+	private _getAttributes(element: Element): string {
+		const attributes = [];
+		for (const attribute of Object.values(element._attributes)) {
+			if (attribute.value !== null) {
+				attributes.push(attribute.name + '="' + encode(attribute.value) + '"');
+			}
+		}
+		return attributes.length > 0 ? ' ' + attributes.join(' ') : '';
+	}
+}
diff --git a/packages/server-rendering/src/context/HappyDOMContext.ts b/packages/server-rendering/src/context/HappyDOMContext.ts
index 8d2f2627..31687f76 100644
--- a/packages/server-rendering/src/context/HappyDOMContext.ts
+++ b/packages/server-rendering/src/context/HappyDOMContext.ts
@@ -62,11 +62,9 @@ export default class HappyDOMContext {
 				.whenAsyncComplete()
 				.then(() => {
 					if (customElements.addCSSToHead) {
-						resolve(
-							this.getResultWithCssAddedToHead(renderer.getOuterHTML(document.documentElement))
-						);
+						resolve(this.getResultWithCssAddedToHead(renderer.render(document)));
 					} else {
-						resolve(renderer.getOuterHTML(document.documentElement));
+						resolve(renderer.render(document));
 					}
 				})
 				.catch(reject);
diff --git a/packages/server-rendering/src/renderer/HappyDOMServerRenderer.ts b/packages/server-rendering/src/renderer/HappyDOMServerRenderer.ts
index 07de5c83..6647a363 100644
--- a/packages/server-rendering/src/renderer/HappyDOMServerRenderer.ts
+++ b/packages/server-rendering/src/renderer/HappyDOMServerRenderer.ts
@@ -1,7 +1,8 @@
 import {
+	Node,
 	Element,
 	CommentNode,
-	HTMLTemplateElement,
+	DocumentType,
 	DocumentFragment,
 	ShadowRoot,
 	SelfClosingHTMLElements,
@@ -34,57 +35,49 @@ export default class HappyDOMServerRenderer {
 	/**
 	 * Renders an element as HTML.
 	 *
-	 * @param element Element to render.
+	 * @param node Node to render.
 	 * @return Result.
 	 */
-	public getOuterHTML(element: Element): HappyDOMServerRenderResult {
-		const tagName = element.tagName.toLowerCase();
+	public render(root: Node): HappyDOMServerRenderResult {
 		const result = new HappyDOMServerRenderResult();
 
-		if (UnclosedHTMLElements.includes(tagName)) {
-			result.html = `<${tagName}${this.getAttributes(element)}>`;
-		} else if (SelfClosingHTMLElements.includes(tagName)) {
-			result.html = `<${tagName}${this.getAttributes(element)}/>`;
-		} else {
-			let innerElement: Element | ShadowRoot = element;
-			let outerElement: Element | ShadowRoot = element;
+		if (root instanceof Element) {
+			const tagName = root.tagName.toLowerCase();
+			if (UnclosedHTMLElements.includes(tagName)) {
+				result.html = `<${tagName}${this._getAttributes(root)}>`;
+			} else if (SelfClosingHTMLElements.includes(tagName)) {
+				result.html = `<${tagName}${this._getAttributes(root)}/>`;
+			} else {
+				let innerElement: Element | ShadowRoot = root;
+				let outerElement: Element | ShadowRoot = root;
+				let xml = '';
 
-			if (this.renderOptions.openShadowRoots && element instanceof Element && element.shadowRoot) {
-				outerElement = this.shadowRootRenderer.getScopedClone(element);
-				innerElement = outerElement.shadowRoot;
-			}
-
-			const innerHTML = this.getInnerHTML(innerElement).html;
-			result.html = `<${tagName}${this.getAttributes(outerElement)}>${innerHTML}</${tagName}>`;
-		}
+				if (this.renderOptions.openShadowRoots && root.shadowRoot) {
+					outerElement = this.shadowRootRenderer.getScopedClone(root);
+					innerElement = outerElement.shadowRoot;
+				}
 
-		if (this.renderOptions.openShadowRoots) {
-			result.css = this.shadowRootRenderer.getScopedCSS();
-		}
-
-		return result;
-	}
+				for (const node of innerElement.childNodes) {
+					xml += this.render(node).html;
+				}
 
-	/**
-	 * Renders an element as HTML.
-	 *
-	 * @param element Element to render.
-	 * @return Result.
-	 */
-	public getInnerHTML(
-		element: Element | DocumentFragment | ShadowRoot
-	): HappyDOMServerRenderResult {
-		const result = new HappyDOMServerRenderResult();
-		const renderElement = (<HTMLTemplateElement>element).content || element;
-
-		for (const child of renderElement.childNodes.slice()) {
-			if (child instanceof Element) {
-				result.html += this.getOuterHTML(child).html;
-			} else if (child instanceof CommentNode) {
-				result.html += '<!--' + child._textContent + '-->';
-			} else if (child['_textContent']) {
-				result.html += child['_textContent'];
+				result.html = `<${tagName}${this._getAttributes(outerElement)}>${xml}</${tagName}>`;
+			}
+		} else if (root instanceof DocumentFragment) {
+			let xml = '';
+			for (const node of root.childNodes) {
+				xml += this.render(node).html;
 			}
+			result.html = xml;
+		} else if (root instanceof CommentNode) {
+			result.html = `<!--${root._textContent}-->`;
+		} else if (root instanceof DocumentType) {
+			const identifier = root.publicId ? ' PUBLIC' : root.systemId ? ' SYSTEM' : '';
+			const publicId = root.publicId ? ` "${root.publicId}"` : '';
+			const systemId = root.systemId ? ` "${root.systemId}"` : '';
+			result.html = `<!DOCTYPE ${root.name}${identifier}${publicId}${systemId}>`;
+		} else if (root['_textContent']) {
+			result.html = root['_textContent'];
 		}
 
 		if (this.renderOptions.openShadowRoots) {
@@ -100,11 +93,11 @@ export default class HappyDOMServerRenderer {
 	 * @param element Element.
 	 * @return Attributes.
 	 */
-	private getAttributes(element: Element): string {
+	private _getAttributes(element: Element): string {
 		const attributes = [];
-		for (const name of Object.keys(element._attributes)) {
-			if (element._attributes[name] && element._attributes[name].value !== null) {
-				attributes.push(name + '="' + encode(element._attributes[name].value) + '"');
+		for (const attribute of Object.values(element._attributes)) {
+			if (attribute.value !== null) {
+				attributes.push(attribute.name + '="' + encode(attribute.value) + '"');
 			}
 		}
 		return attributes.length > 0 ? ' ' + attributes.join(' ') : '';


Test patch:
diff --git a/packages/happy-dom/test/dom-parser/DOMParser.test.ts b/packages/happy-dom/test/dom-parser/DOMParser.test.ts
index 96df87d5..7ea539ca 100644
--- a/packages/happy-dom/test/dom-parser/DOMParser.test.ts
+++ b/packages/happy-dom/test/dom-parser/DOMParser.test.ts
@@ -1,4 +1,5 @@
 import Window from '../../src/window/Window';
+import XMLSerializer from '../../src/xml-serializer/XMLSerializer';
 import DOMParserHTML from './data/DOMParserHTML';
 
 describe('DOMParser', () => {
@@ -12,12 +13,14 @@ describe('DOMParser', () => {
 	describe('parseFromString()', () => {
 		test('Parses HTML of a page and returns a new document.', () => {
 			const newDocument = domParser.parseFromString(DOMParserHTML, 'text/html');
-			expect(newDocument.documentElement.outerHTML).toBe(DOMParserHTML);
+			expect(new XMLSerializer().serializeToString(newDocument).replace(/[\s]/gm, '')).toBe(
+				DOMParserHTML.replace(/[\s]/gm, '')
+			);
 		});
 
 		test('Parses HTML with just a string and returns a new document with <html>, <head> and <body> tags.', () => {
 			const newDocument = domParser.parseFromString('Test', 'text/html');
-			expect(newDocument.documentElement.outerHTML).toBe(
+			expect(new XMLSerializer().serializeToString(newDocument)).toBe(
 				'<html><head></head><body>Test</body></html>'
 			);
 		});
@@ -37,7 +40,7 @@ describe('DOMParser', () => {
 			`,
 				'text/html'
 			);
-			expect(newDocument.documentElement.outerHTML.replace(/[\s]/gm, '')).toBe(
+			expect(new XMLSerializer().serializeToString(newDocument).replace(/[\s]/gm, '')).toBe(
 				`
 				<html>
 					<head>
diff --git a/packages/happy-dom/test/dom-parser/data/DOMParserHTML.ts b/packages/happy-dom/test/dom-parser/data/DOMParserHTML.ts
index 06f7cb24..870f7835 100644
--- a/packages/happy-dom/test/dom-parser/data/DOMParserHTML.ts
+++ b/packages/happy-dom/test/dom-parser/data/DOMParserHTML.ts
@@ -1,4 +1,5 @@
 export default `
+	<!DOCTYPE html>
 	<html>
 		<head>
 			<title>Title</title>
diff --git a/packages/happy-dom/test/nodes/basic/element/Element.test.ts b/packages/happy-dom/test/nodes/basic/element/Element.test.ts
index 1b739ba3..d83e54db 100644
--- a/packages/happy-dom/test/nodes/basic/element/Element.test.ts
+++ b/packages/happy-dom/test/nodes/basic/element/Element.test.ts
@@ -1,6 +1,6 @@
 import Window from '../../../../src/window/Window';
-import HTMLRenderer from '../../../../src/html-renderer/HTMLRenderer';
-import HTMLParser from '../../../../src/html-parser/HTMLParser';
+import XMLSerializer from '../../../../src/xml-serializer/XMLSerializer';
+import XMLParser from '../../../../src/xml-parser/XMLParser';
 import CustomElement from '../../../CustomElement';
 import ShadowRoot from '../../../../src/nodes/basic/shadow-root/ShadowRoot';
 import Document from '../../../../src/nodes/basic/document/Document';
@@ -113,8 +113,12 @@ describe('Element', () => {
 
 	describe('get innerHTML()', () => {
 		test('Returns HTML of children as a concatenated string.', () => {
-			jest.spyOn(HTMLRenderer, 'getInnerHTML').mockImplementation(renderElement => {
-				expect(renderElement).toBe(element);
+			const div = document.createElement('div');
+
+			element.appendChild(div);
+
+			jest.spyOn(XMLSerializer.prototype, 'serializeToString').mockImplementation(rootElement => {
+				expect(rootElement).toBe(div);
 				return 'EXPECTED_HTML';
 			});
 
@@ -132,7 +136,7 @@ describe('Element', () => {
 			div.appendChild(textNode);
 			root.appendChild(div);
 
-			jest.spyOn(HTMLParser, 'parse').mockImplementation((parseDocument, html) => {
+			jest.spyOn(XMLParser, 'parse').mockImplementation((parseDocument, html) => {
 				expect(parseDocument).toBe(document);
 				expect(html).toBe('SOME_HTML');
 				return root;
diff --git a/packages/happy-dom/test/html-parser/HTMLParser.test.ts b/packages/happy-dom/test/xml-parser/XMLParser.test.ts
similarity index 69%
rename from packages/happy-dom/test/html-parser/HTMLParser.test.ts
rename to packages/happy-dom/test/xml-parser/XMLParser.test.ts
index 2d03467f..0d3d372c 100644
--- a/packages/happy-dom/test/html-parser/HTMLParser.test.ts
+++ b/packages/happy-dom/test/xml-parser/XMLParser.test.ts
@@ -1,10 +1,11 @@
-import HTMLParser from '../../src/html-parser/HTMLParser';
+import XMLParser from '../../src/xml-parser/XMLParser';
 import Window from '../../src/window/Window';
 import HTMLElement from '../../src/nodes/basic/html-element/HTMLElement';
-import HTMLParserHTML from './data/HTMLParserHTML';
+import XMLParserHTML from './data/XMLParserHTML';
 import NamespaceURI from '../../src/html-config/NamespaceURI';
+import DocumentType from '../../src/nodes/basic/document-type/DocumentType';
 
-describe('HTMLParser', () => {
+describe('XMLParser', () => {
 	let window: Window;
 
 	beforeEach(() => {
@@ -13,14 +14,14 @@ describe('HTMLParser', () => {
 
 	describe('parse()', () => {
 		test('Parses HTML with a single <div>.', () => {
-			const root = HTMLParser.parse(window.document, '<div></div>');
+			const root = XMLParser.parse(window.document, '<div></div>');
 			expect(root.childNodes.length).toBe(1);
 			expect(root.childNodes[0].childNodes.length).toBe(0);
 			expect((<HTMLElement>root.childNodes[0]).tagName).toBe('DIV');
 		});
 
 		test('Parses HTML with a single <div> with attributes.', () => {
-			const root = HTMLParser.parse(
+			const root = XMLParser.parse(
 				window.document,
 				'<div class="class1 class2" id="id" data-no-value></div>'
 			);
@@ -41,12 +42,38 @@ describe('HTMLParser', () => {
 		});
 
 		test('Parses an entire HTML page.', () => {
-			const root = HTMLParser.parse(window.document, HTMLParserHTML);
-			expect(root.innerHTML).toBe(HTMLParserHTML);
+			const root = XMLParser.parse(window.document, XMLParserHTML);
+			expect(root.innerHTML.replace(/[\s]/gm, '')).toBe(XMLParserHTML.replace(/[\s]/gm, ''));
+		});
+
+		test('Parses a page with document type set to "HTML 4.01".', () => {
+			const pageHTML = XMLParserHTML.trim().replace(
+				'<!DOCTYPE html>',
+				'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">'
+			);
+			const root = XMLParser.parse(window.document, pageHTML);
+			const doctype = <DocumentType>root.childNodes[0];
+			expect(doctype.name).toBe('HTML');
+			expect(doctype.publicId).toBe('-//W3C//DTD HTML 4.01//EN');
+			expect(doctype.systemId).toBe('http://www.w3.org/TR/html4/strict.dtd');
+			expect(root.innerHTML.replace(/[\s]/gm, '')).toBe(pageHTML.replace(/[\s]/gm, ''));
+		});
+
+		test('Parses a page with document type set to "MathML 1.01".', () => {
+			const pageHTML = XMLParserHTML.trim().replace(
+				'<!DOCTYPE html>',
+				'<!DOCTYPE math SYSTEM "http://www.w3.org/Math/DTD/mathml1/mathml.dtd">'
+			);
+			const root = XMLParser.parse(window.document, pageHTML);
+			const doctype = <DocumentType>root.childNodes[0];
+			expect(doctype.name).toBe('math');
+			expect(doctype.publicId).toBe('');
+			expect(doctype.systemId).toBe('http://www.w3.org/Math/DTD/mathml1/mathml.dtd');
+			expect(root.innerHTML.replace(/[\s]/gm, '')).toBe(pageHTML.replace(/[\s]/gm, ''));
 		});
 
 		test('Handles unclosed tags of unnestable elements (e.g. <a>, <li>).', () => {
-			const root = HTMLParser.parse(
+			const root = XMLParser.parse(
 				window.document,
 				`
 				<div class="test" disabled>
@@ -75,7 +102,7 @@ describe('HTMLParser', () => {
 		});
 
 		test('Parses an SVG with "xmlns" set to HTML.', () => {
-			const root = HTMLParser.parse(
+			const root = XMLParser.parse(
 				window.document,
 				`
 				<div>
diff --git a/packages/happy-dom/test/html-parser/data/HTMLParserHTML.ts b/packages/happy-dom/test/xml-parser/data/XMLParserHTML.ts
similarity index 95%
rename from packages/happy-dom/test/html-parser/data/HTMLParserHTML.ts
rename to packages/happy-dom/test/xml-parser/data/XMLParserHTML.ts
index 06f7cb24..870f7835 100644
--- a/packages/happy-dom/test/html-parser/data/HTMLParserHTML.ts
+++ b/packages/happy-dom/test/xml-parser/data/XMLParserHTML.ts
@@ -1,4 +1,5 @@
 export default `
+	<!DOCTYPE html>
 	<html>
 		<head>
 			<title>Title</title>
diff --git a/packages/happy-dom/test/html-renderer/HTMLRenderer.test.ts b/packages/happy-dom/test/xml-serializer/XMLSerializer.test.ts
similarity index 72%
rename from packages/happy-dom/test/html-renderer/HTMLRenderer.test.ts
rename to packages/happy-dom/test/xml-serializer/XMLSerializer.test.ts
index 3d60a267..49f1c937 100644
--- a/packages/happy-dom/test/html-renderer/HTMLRenderer.test.ts
+++ b/packages/happy-dom/test/xml-serializer/XMLSerializer.test.ts
@@ -1,16 +1,20 @@
-import HTMLRenderer from '../../src/html-renderer/HTMLRenderer';
+import XMLSerializer from '../../src/xml-serializer/XMLSerializer';
 import Window from '../../src/window/Window';
+import Document from '../../src/nodes/basic/document/Document';
 
-describe('HTMLRenderer', () => {
-	let window, document;
+describe('XMLSerializer', () => {
+	let window: Window;
+	let document: Document;
+	let xmlSerializer: XMLSerializer;
 
 	beforeEach(() => {
 		window = new Window();
 		document = window.document;
+		xmlSerializer = new XMLSerializer();
 	});
 
-	describe('getOuterHTML()', () => {
-		test('Renders a <div> element.', () => {
+	describe('serializeToString()', () => {
+		test('Serializes a <div> element.', () => {
 			const div = document.createElement('div');
 			const span = document.createElement('span');
 
@@ -23,23 +27,23 @@ describe('HTMLRenderer', () => {
 			div.setAttribute('attr3', '');
 			div.appendChild(span);
 
-			expect(HTMLRenderer.getOuterHTML(div)).toBe(
+			expect(xmlSerializer.serializeToString(div)).toBe(
 				'<div attr1="value1" attr2="value2" attr3=""><span attr1="value1" attr2="value2" attr3=""></span></div>'
 			);
 		});
 
-		test('Renders a comment node.', () => {
+		test('Serializes a comment node.', () => {
 			const div = document.createElement('div');
-			const comment = document.createComment();
+			const comment = document.createComment('');
 
 			comment.textContent = 'Some comment.';
 
 			div.appendChild(comment);
 
-			expect(HTMLRenderer.getOuterHTML(div)).toBe('<div><!--Some comment.--></div>');
+			expect(xmlSerializer.serializeToString(div)).toBe('<div><!--Some comment.--></div>');
 		});
 
-		test('Renders a text nodes.', () => {
+		test('Serializes a text nodes.', () => {
 			const div = document.createElement('div');
 			const text1 = document.createTextNode('Text 1.');
 			const text2 = document.createTextNode('Text 2.');
@@ -47,10 +51,10 @@ describe('HTMLRenderer', () => {
 			div.appendChild(text1);
 			div.appendChild(text2);
 
-			expect(HTMLRenderer.getOuterHTML(div)).toBe('<div>Text 1.Text 2.</div>');
+			expect(xmlSerializer.serializeToString(div)).toBe('<div>Text 1.Text 2.</div>');
 		});
 
-		test('Renders a mix of nodes.', () => {
+		test('Serializes a mix of nodes.', () => {
 			const div = document.createElement('div');
 			const comment1 = document.createComment('Comment 1.');
 			const comment2 = document.createComment('Comment 2.');
@@ -74,12 +78,12 @@ describe('HTMLRenderer', () => {
 			div.appendChild(text2);
 			div.appendChild(span1);
 
-			expect(HTMLRenderer.getOuterHTML(div)).toBe(
+			expect(xmlSerializer.serializeToString(div)).toBe(
 				'<div><!--Comment 1.-->Text 1.<!--Comment 2.-->Text 2.<span attr1="value1" attr2="value2" attr3=""><span attr1="value1">Text 3.</span></span></div>'
 			);
 		});
 
-		test('Renders a custom element closed.', () => {
+		test('Serializes a closed custom element.', () => {
 			const div = document.createElement('div');
 			const customElement = document.createElement('custom-element');
 
@@ -92,7 +96,7 @@ describe('HTMLRenderer', () => {
 			// Connects the custom element to DOM which will trigger connectedCallback() on it
 			document.body.appendChild(div);
 
-			expect(HTMLRenderer.getOuterHTML(div)).toBe(
+			expect(xmlSerializer.serializeToString(div)).toBe(
 				'<div><custom-element attr1="value1" attr2="value2" attr3=""></custom-element></div>'
 			);
 		});
diff --git a/packages/server-rendering/test/renderer/HappyDOMServerRenderer.test.ts b/packages/server-rendering/test/renderer/HappyDOMServerRenderer.test.ts
index d2bf3112..6e20aa9c 100644
--- a/packages/server-rendering/test/renderer/HappyDOMServerRenderer.test.ts
+++ b/packages/server-rendering/test/renderer/HappyDOMServerRenderer.test.ts
@@ -11,7 +11,7 @@ describe('HappyDOMServerRenderer', () => {
 		document = window.document;
 	});
 
-	describe('getOuterHTML()', () => {
+	describe('render()', () => {
 		test('Renders a <div> element.', () => {
 			const div = document.createElement('div');
 			const span = document.createElement('span');
@@ -25,7 +25,7 @@ describe('HappyDOMServerRenderer', () => {
 			div.setAttribute('attr3', '');
 			div.appendChild(span);
 
-			expect(new HappyDOMServerRenderer().getOuterHTML(div).html).toBe(
+			expect(new HappyDOMServerRenderer().render(div).html).toBe(
 				'<div attr1="value1" attr2="value2" attr3=""><span attr1="value1" attr2="value2" attr3=""></span></div>'
 			);
 		});
@@ -38,9 +38,7 @@ describe('HappyDOMServerRenderer', () => {
 
 			div.appendChild(comment);
 
-			expect(new HappyDOMServerRenderer().getOuterHTML(div).html).toBe(
-				'<div><!--Some comment.--></div>'
-			);
+			expect(new HappyDOMServerRenderer().render(div).html).toBe('<div><!--Some comment.--></div>');
 		});
 
 		test('Renders a text nodes.', () => {
@@ -51,7 +49,7 @@ describe('HappyDOMServerRenderer', () => {
 			div.appendChild(text1);
 			div.appendChild(text2);
 
-			expect(new HappyDOMServerRenderer().getOuterHTML(div).html).toBe('<div>Text 1.Text 2.</div>');
+			expect(new HappyDOMServerRenderer().render(div).html).toBe('<div>Text 1.Text 2.</div>');
 		});
 
 		test('Renders a mix of nodes.', () => {
@@ -78,7 +76,7 @@ describe('HappyDOMServerRenderer', () => {
 			div.appendChild(text2);
 			div.appendChild(span1);
 
-			expect(new HappyDOMServerRenderer().getOuterHTML(div).html).toBe(
+			expect(new HappyDOMServerRenderer().render(div).html).toBe(
 				'<div><!--Comment 1.-->Text 1.<!--Comment 2.-->Text 2.<span attr1="value1" attr2="value2" attr3=""><span attr1="value1">Text 3.</span></span></div>'
 			);
 		});
@@ -96,7 +94,7 @@ describe('HappyDOMServerRenderer', () => {
 			// Connects the custom element to DOM which will trigger connectedCallback() on it
 			document.body.appendChild(div);
 
-			expect(new HappyDOMServerRenderer().getOuterHTML(div).html).toBe(
+			expect(new HappyDOMServerRenderer().render(div).html).toBe(
 				'<div><mocked-html-element attr1="value1" attr2="value2" attr3=""></mocked-html-element></div>'
 			);
 		});
@@ -118,7 +116,7 @@ describe('HappyDOMServerRenderer', () => {
 				openShadowRoots: true,
 				extractCSS: false,
 				scopeCSS: false
-			}).getOuterHTML(div);
+			}).render(div);
 
 			expect(result.html.replace(/\s/gm, '')).toBe(
 				`
@@ -178,7 +176,7 @@ describe('HappyDOMServerRenderer', () => {
 				openShadowRoots: true,
 				extractCSS: false,
 				scopeCSS: true
-			}).getOuterHTML(div);
+			}).render(div);
 
 			expect(result.html.replace(/\s/gm, '')).toBe(
 				`
@@ -240,7 +238,7 @@ describe('HappyDOMServerRenderer', () => {
 				openShadowRoots: true,
 				extractCSS: true,
 				scopeCSS: true
-			}).getOuterHTML(div);
+			}).render(div);
 
 			expect(result.html.replace(/\s/gm, '')).toBe(
 				`