Repository: capricorn86/happy-dom
Pull request #403 (2022-03-14 16:20:13)

#251@minor: Adds support for Declarative Shadow DOM.


Patch:
diff --git a/README.md b/README.md
index d3eabcc0..69bff11a 100644
--- a/README.md
+++ b/README.md
@@ -3,9 +3,12 @@
 
 # About
 
-A [JSDOM](https://github.com/jsdom/jsdom) alternative with focus on performance.
+[Happy DOM](https://github.com/capricorn86/happy-dom) is a JavaScript implementation of a web browser without its graphical user interface. It includes many web standards from WHATWG [DOM](https://dom.spec.whatwg.org/) and [HTML](https://html.spec.whatwg.org/multipage/).
+
+The goal of [Happy DOM](https://github.com/capricorn86/happy-dom) is to emulate enough of a web browser to be useful for testing, scraping web sites and server-side rendering.
+
+[Happy DOM](https://github.com/capricorn86/happy-dom) focuses heavily on performance and can be used as an alternative to [JSDOM](https://github.com/jsdom/jsdom).
 
-Happy DOM aim to support the most common functionality of a web browser.
 
 [Read more about how to use Happy DOM](https://github.com/capricorn86/happy-dom/tree/master/packages/happy-dom)
 
@@ -17,6 +20,8 @@ Happy DOM aim to support the most common functionality of a web browser.
 
 - Shadow Root (Shadow DOM)
 
+- Declarative Shadow DOM
+
 - Mutation Observer
 
 - Tree Walker
@@ -58,15 +63,17 @@ This package makes it possible to use Happy DOM with [Jest](https://jestjs.io/).
 
 ---
 
-### [![Published on npm](https://img.shields.io/npm/v/@happy-dom/server-rendering.svg)](https://www.npmjs.com/package/@happy-dom/server-rendering) [server-rendering](https://github.com/capricorn86/happy-dom/tree/master/packages/server-rendering)
+### [![Published on npm](https://img.shields.io/npm/v/@happy-dom/global-registrator.svg)](https://www.npmjs.com/package/@happy-dom/global-registrator) [global-registrator](https://github.com/capricorn86/happy-dom/tree/master/packages/global-registrator)
 
-This package makes it easier to setup servering side rendering of web components by handling the setup of the Node [VM Context](https://nodejs.org/api/vm.html#vm_vm_createcontext_sandbox_options) for you.
+A utility that registers Happy DOM globally, which makes it possible to use Happy DOM for testing in a Node environment.
 
 ---
 
-### [![Published on npm](https://img.shields.io/npm/v/@happy-dom/global-registrator.svg)](https://www.npmjs.com/package/@happy-dom/global-registrator) [global-registrator](https://github.com/capricorn86/happy-dom/tree/master/packages/global-registrator)
+### [![Published on npm](https://img.shields.io/npm/v/@happy-dom/server-rendering.svg)](https://www.npmjs.com/package/@happy-dom/server-rendering) [server-rendering](https://github.com/capricorn86/happy-dom/tree/master/packages/server-rendering)
 
-A utility that registers Happy DOM globally, which makes it possible to use Happy DOM for testing in a Node environment.
+This package has been deprecated.
+
+Happy DOM now supports [Declarative Shadow DOM](https://github.com/capricorn86/happy-dom/tree/master/packages/happy-dom#server-side-rendering-of-web-components) which can be used for server-side rendering of web components.
 
 
 
@@ -86,32 +93,6 @@ A utility that registers Happy DOM globally, which makes it possible to use Happ
 
 [See how the test was done here](https://github.com/capricorn86/happy-dom-performance-test)
 
-
-
-# Whats New in v2.0.0?
-
-- The methods window.whenAsyncComplete() and window.cancelAsync() has been moved to window.happyDOM.whenAsyncComplete() and window.happyDOM.cancelAsync()
-
-# Whats New in v1.0.0?
-
-- [Lerna](https://lerna.js.org/) is used for managing all packages within a single repository
-
-- Support for [React](https://reactjs.org), [Angular](https://angular.io/), [Vue](https://vuejs.org/)
-
-- Full support for [querySelector()](https://www.w3.org/TR/selectors-api/#queryselector) and [querySelectorAll()](https://www.w3.org/TR/selectors-api/#queryselectorall)
-
-- Server side rendering has been split out to its own package
-
-- All functionality is now covered by unit tests
-
-- Automated release process by publishing to NPM automatically when a pull request is merged
-
-- Release notes are generated automatically by using [Github Releases](https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/about-releases)
-
-- A lot of minor bug fixes
-
-
-
 # Contributing
 
 [Read more about how to develop and contribute](https://github.com/capricorn86/happy-dom/blob/master/docs/contributing.md)
\ No newline at end of file
diff --git a/packages/global-registrator/README.md b/packages/global-registrator/README.md
index bd53710a..19fc3c46 100644
--- a/packages/global-registrator/README.md
+++ b/packages/global-registrator/README.md
@@ -3,9 +3,11 @@
 
 # About
 
-A [JSDOM](https://github.com/jsdom/jsdom) alternative with focus on performance.
+[Happy DOM](https://github.com/capricorn86/happy-dom) is a JavaScript implementation of a web browser without its graphical user interface. It includes many web standards from WHATWG [DOM](https://dom.spec.whatwg.org/) and [HTML](https://html.spec.whatwg.org/multipage/).
 
-[Happy DOM](https://github.com/capricorn86/happy-dom) aim to support the most common functionality of a web browser.
+The goal of [Happy DOM](https://github.com/capricorn86/happy-dom) is to emulate enough of a web browser to be useful for testing, scraping web sites and server-side rendering.
+
+[Happy DOM](https://github.com/capricorn86/happy-dom) focuses heavily on performance and can be used as an alternative to [JSDOM](https://github.com/jsdom/jsdom).
 
 This package contains a utility that registers [Happy DOM](https://github.com/capricorn86/happy-dom) globally, which makes it possible to use [Happy DOM](https://github.com/capricorn86/happy-dom) for testing in a Node environment.
 
@@ -16,6 +18,8 @@ This package contains a utility that registers [Happy DOM](https://github.com/ca
 
 - Shadow Root (Shadow DOM)
 
+- Declarative Shadow DOM
+
 - Mutation Observer
 
 - Tree Walker
diff --git a/packages/happy-dom/README.md b/packages/happy-dom/README.md
index aa22d5f1..2a333edc 100644
--- a/packages/happy-dom/README.md
+++ b/packages/happy-dom/README.md
@@ -3,9 +3,11 @@
 
 # About
 
-A [JSDOM](https://github.com/jsdom/jsdom) alternative with focus on performance.
+[Happy DOM](https://github.com/capricorn86/happy-dom) is a JavaScript implementation of a web browser without its graphical user interface. It includes many web standards from WHATWG [DOM](https://dom.spec.whatwg.org/) and [HTML](https://html.spec.whatwg.org/multipage/).
 
-[Happy DOM](https://github.com/capricorn86/happy-dom) aim to support the most common functionality of a web browser.
+The goal of [Happy DOM](https://github.com/capricorn86/happy-dom) is to emulate enough of a web browser to be useful for testing, scraping web sites and server-side rendering.
+
+[Happy DOM](https://github.com/capricorn86/happy-dom) focuses heavily on performance and can be used as an alternative to [JSDOM](https://github.com/jsdom/jsdom).
 
 
 ### DOM Features
@@ -14,6 +16,8 @@ A [JSDOM](https://github.com/jsdom/jsdom) alternative with focus on performance.
 
 - Shadow Root (Shadow DOM)
 
+- Declarative Shadow DOM
+
 - Mutation Observer
 
 - Tree Walker
@@ -95,13 +99,13 @@ document.write(`
              <div class="container">
                   <!–– Content will be added here -->
              </div>
+            <script>
+                const element = document.createElement('div');
+                const container = document.querySelector('.container');
+                element.innerHTML = 'Test';
+                container.appendChild(element);
+            </script>
         </body>
-        <script>
-            const element = document.createElement('div');
-            const container = document.querySelector('.container');
-            element.innerHTML = 'Test';
-            container.appendChild(element);
-        </script>
     </html>
 `);
 
@@ -109,6 +113,76 @@ document.write(`
 console.log(document.querySelector('.container div').innerHTML);
 ```
 
+## Server-Side Rendering of Web Components
+
+The example below will show you how to setup a Node [VM context](https://nodejs.org/api/vm.html#vm_vm_createcontext_sandbox_options) to render a page with custom elements (web components) in Happy DOM. We can then use a new web feature called [Declarative Shadow DOM](https://chromestatus.com/feature/5191745052606464) to include the shadow roots in the HTML output.
+
+[Declarative Shadow DOM](https://chromestatus.com/feature/5191745052606464) is only supported by Chromium based browsers. Unsupported browsers should safely fallback to being rendered using Javascript.
+
+```javascript
+import { Window } from 'happy-dom';
+import VM from 'vm';
+
+const window = VM.createContext(new Window());
+const document = window.document;
+
+window.location.href = 'http://localhost:8080';
+
+document.write(`
+    <html>
+        <head>
+             <title>Test page</title>
+        </head>
+        <body>
+            <div>
+                <my-custom-element>
+                    <span>Slotted content</span>
+                </my-custom-element>
+            </div>
+            <script>
+                class MyCustomElement extends HTMLElement {
+                    constructor() {
+                        super();
+                        this.attachShadow({ mode: 'open' });
+                    }
+
+                    connectedCallback() {
+                        this.shadowRoot.innerHTML = \`
+                            <style>
+                                :host {
+                                    display: inline-block;
+                                    background: red;
+                                }
+                            </style>
+                            <div><slot></slot></div>
+                        \`;
+                    }
+                }
+
+                customElements.define('my-custom-element', MyCustomElement);
+            </script>
+        </body>
+    </html>
+`);
+
+/*
+Will output:
+<my-custom-element>
+    <span>Slotted content</span>
+    <template shadowroot="open">
+        <style>
+            :host {
+                display: inline-block;
+                background: red;
+            }
+        </style>
+        <div><slot></slot></div>
+    </template>
+</my-custom-element>
+*/
+console.log(document.body.querySelector('div').getInnerHTML({ includeShadowRoots: true }));
+```
+
 
 
 ## Additional Features
@@ -160,8 +234,6 @@ window.happyDOM.cancelAsync();
 
 Happy DOM provide with a package called [@happy-dom/jest-environment](https://github.com/capricorn86/happy-dom/tree/master/packages/jest-environment) that makes it possible to use Happy DOM with [Jest](https://jestjs.io/).
 
+# Global Registration
 
-
-# Server Side Rendering
-
-Happy DOM provide with a package called [@happy-dom/server-rendering](https://github.com/capricorn86/happy-dom/tree/master/packages/server-rendering) that makes the setup of server side rendering easier.
\ No newline at end of file
+Happy DOM provide with a package called [@happy-dom/global-registrator](https://github.com/capricorn86/happy-dom/tree/master/packages/global-registrator) that can register Happy DOM globally. It makes it possible to use Happy DOM for testing in a Node environment.
\ No newline at end of file
diff --git a/packages/happy-dom/src/nodes/element/Element.ts b/packages/happy-dom/src/nodes/element/Element.ts
index ee717314..68021f09 100644
--- a/packages/happy-dom/src/nodes/element/Element.ts
+++ b/packages/happy-dom/src/nodes/element/Element.ts
@@ -37,12 +37,16 @@ export default class Element extends Node implements IElement {
 	public tagName: string = null;
 	public nodeType = Node.ELEMENT_NODE;
 	public shadowRoot: IShadowRoot = null;
-	public _attributes: { [k: string]: Attr } = {};
+
 	public scrollTop = 0;
 	public scrollLeft = 0;
 	public children: IHTMLCollection<IElement> = HTMLCollectionFactory.create();
 	public readonly namespaceURI: string = null;
 
+	// Used for being able to access closed shadow roots
+	public _shadowRoot: IShadowRoot = null;
+	public _attributes: { [k: string]: Attr } = {};
+
 	private _classList: DOMTokenList = null;
 
 	/**
@@ -271,6 +275,26 @@ export default class Element extends Node implements IElement {
 	 */
 	public attributeChangedCallback?(name: string, oldValue: string, newValue: string): void;
 
+	/**
+	 * Returns inner HTML and optionally the content of shadow roots.
+	 *
+	 * This is a feature implemented in Chromium, but not supported by Mozilla yet.
+	 *
+	 * @see https://web.dev/declarative-shadow-dom/
+	 * @see https://chromestatus.com/feature/5191745052606464
+	 * @param [options] Options.
+	 * @param [options.includeShadowRoots] Set to "true" to include shadow roots.
+	 * @returns HTML.
+	 */
+	public getInnerHTML(options?: { includeShadowRoots?: boolean }): string {
+		const xmlSerializer = new XMLSerializer();
+		let xml = '';
+		for (const node of this.childNodes) {
+			xml += xmlSerializer.serializeToString(node, options);
+		}
+		return xml;
+	}
+
 	/**
 	 * Clones a node.
 	 *
@@ -628,15 +652,21 @@ export default class Element extends Node implements IElement {
 	 * @returns Shadow root.
 	 */
 	public attachShadow(shadowRootInit: { mode: string }): IShadowRoot {
-		if (this.shadowRoot) {
+		if (this._shadowRoot) {
 			throw new DOMException('Shadow root has already been attached.');
 		}
-		(<IShadowRoot>this.shadowRoot) = new ShadowRoot();
-		(<IDocument>this.shadowRoot.ownerDocument) = this.ownerDocument;
-		(<Element>this.shadowRoot.host) = this;
-		(<string>this.shadowRoot.mode) = shadowRootInit.mode;
-		(<ShadowRoot>this.shadowRoot)._connectToNode(this);
-		return this.shadowRoot;
+
+		(<IShadowRoot>this._shadowRoot) = new ShadowRoot();
+		(<IDocument>this._shadowRoot.ownerDocument) = this.ownerDocument;
+		(<Element>this._shadowRoot.host) = this;
+		(<string>this._shadowRoot.mode) = shadowRootInit.mode;
+		(<ShadowRoot>this._shadowRoot)._connectToNode(this);
+
+		if (this._shadowRoot.mode === 'open') {
+			(<IShadowRoot>this.shadowRoot) = this._shadowRoot;
+		}
+
+		return this._shadowRoot;
 	}
 
 	/**
diff --git a/packages/happy-dom/src/nodes/element/IElement.ts b/packages/happy-dom/src/nodes/element/IElement.ts
index e0e3f3b4..146d4f9d 100644
--- a/packages/happy-dom/src/nodes/element/IElement.ts
+++ b/packages/happy-dom/src/nodes/element/IElement.ts
@@ -25,6 +25,8 @@ export default interface IElement extends IChildNode, INonDocumentTypeChildNode,
 	innerHTML: string;
 	outerHTML: string;
 	slot: string;
+	readonly nodeName: string;
+	readonly localName: string;
 	readonly attributes: { [k: string]: Attr | number };
 
 	/**
@@ -36,6 +38,19 @@ export default interface IElement extends IChildNode, INonDocumentTypeChildNode,
 	 */
 	attributeChangedCallback?(name: string, oldValue: string, newValue: string): void;
 
+	/**
+	 * Returns inner HTML and optionally the content of shadow roots.
+	 *
+	 * This is a feature implemented in Chromium, but not supported by Mozilla yet.
+	 *
+	 * @see https://web.dev/declarative-shadow-dom/
+	 * @see https://chromestatus.com/feature/5191745052606464
+	 * @param [options] Options.
+	 * @param [options.includeShadowRoots] Set to "true" to include shadow roots.
+	 * @returns HTML.
+	 */
+	getInnerHTML(options?: { includeShadowRoots?: boolean }): string;
+
 	/**
 	 * Sets an attribute.
 	 *
@@ -53,6 +68,13 @@ export default interface IElement extends IChildNode, INonDocumentTypeChildNode,
 	 */
 	setAttributeNS(namespaceURI: string, name: string, value: string): void;
 
+	/**
+	 * Returns attribute names.
+	 *
+	 * @returns Attribute names.
+	 */
+	getAttributeNames(): string[];
+
 	/**
 	 * Returns attribute value.
 	 *
diff --git a/packages/happy-dom/src/nodes/node/Node.ts b/packages/happy-dom/src/nodes/node/Node.ts
index 11ca3d27..72a684c9 100644
--- a/packages/happy-dom/src/nodes/node/Node.ts
+++ b/packages/happy-dom/src/nodes/node/Node.ts
@@ -491,9 +491,9 @@ export default class Node extends EventTarget implements INode {
 			}
 
 			// eslint-disable-next-line
-			if ((<any>this).shadowRoot) {
+			if ((<any>this)._shadowRoot) {
 				// eslint-disable-next-line
-				(<any>this).shadowRoot._connectToNode(this);
+				(<any>this)._shadowRoot._connectToNode(this);
 			}
 		}
 	}
diff --git a/packages/happy-dom/src/xml-serializer/XMLSerializer.ts b/packages/happy-dom/src/xml-serializer/XMLSerializer.ts
index e3630f94..1c3015cc 100644
--- a/packages/happy-dom/src/xml-serializer/XMLSerializer.ts
+++ b/packages/happy-dom/src/xml-serializer/XMLSerializer.ts
@@ -9,18 +9,17 @@ import IElement from '../nodes/element/IElement';
 
 /**
  * Utility for converting an element to string.
- *
- * @class QuerySelector
  */
 export default class XMLSerializer {
 	/**
 	 * Renders an element as HTML.
 	 *
-	 * @param element Element to render.
-	 * @param root
+	 * @param root Root element.
+	 * @param [options] Options.
+	 * @param [options.includeShadowRoots] Set to "true" to include shadow roots.
 	 * @returns Result.
 	 */
-	public serializeToString(root: INode): string {
+	public serializeToString(root: INode, options?: { includeShadowRoots?: boolean }): string {
 		switch (root.nodeType) {
 			case Node.ELEMENT_NODE:
 				const element = <Element>root;
@@ -33,8 +32,19 @@ export default class XMLSerializer {
 				}
 
 				let innerHTML = '';
+
 				for (const node of root.childNodes) {
-					innerHTML += this.serializeToString(node);
+					innerHTML += this.serializeToString(node, options);
+				}
+
+				if (options?.includeShadowRoots && element.shadowRoot) {
+					innerHTML += `<template shadowroot="${element.shadowRoot.mode}">`;
+
+					for (const node of element.shadowRoot.childNodes) {
+						innerHTML += this.serializeToString(node, options);
+					}
+
+					innerHTML += '</template>';
 				}
 
 				return `<${tagName}${this._getAttributes(element)}>${innerHTML}</${tagName}>`;
@@ -42,7 +52,7 @@ export default class XMLSerializer {
 			case Node.DOCUMENT_NODE:
 				let html = '';
 				for (const node of root.childNodes) {
-					html += this.serializeToString(node);
+					html += this.serializeToString(node, options);
 				}
 				return html;
 			case Node.COMMENT_NODE:
diff --git a/packages/jest-environment/README.md b/packages/jest-environment/README.md
index e8218f47..f5dc153a 100644
--- a/packages/jest-environment/README.md
+++ b/packages/jest-environment/README.md
@@ -3,9 +3,11 @@
 
 # About
 
-A [JSDOM](https://github.com/jsdom/jsdom) alternative with focus on performance.
+[Happy DOM](https://github.com/capricorn86/happy-dom) is a JavaScript implementation of a web browser without its graphical user interface. It includes many web standards from WHATWG [DOM](https://dom.spec.whatwg.org/) and [HTML](https://html.spec.whatwg.org/multipage/).
 
-[Happy DOM](https://github.com/capricorn86/happy-dom) aim to support the most common functionality of a web browser.
+The goal of [Happy DOM](https://github.com/capricorn86/happy-dom) is to emulate enough of a web browser to be useful for testing, scraping web sites and server-side rendering.
+
+[Happy DOM](https://github.com/capricorn86/happy-dom) focuses heavily on performance and can be used as an alternative to [JSDOM](https://github.com/jsdom/jsdom).
 
 This package makes it possible to use [Happy DOM](https://github.com/capricorn86/happy-dom) with [Jest](https://jestjs.io/).
 
@@ -16,6 +18,8 @@ This package makes it possible to use [Happy DOM](https://github.com/capricorn86
 
 - Shadow Root (Shadow DOM)
 
+- Declarative Shadow DOM
+
 - Mutation Observer
 
 - Tree Walker
diff --git a/packages/server-rendering/README.md b/packages/server-rendering/README.md
index 71993f98..e8a4734a 100644
--- a/packages/server-rendering/README.md
+++ b/packages/server-rendering/README.md
@@ -1,11 +1,16 @@
+:warning: **This package is deprecated. Happy DOM now supports [Declarative Shadow DOM](https://github.com/capricorn86/happy-dom/tree/master/packages/happy-dom#server-side-rendering-of-web-components) which can be used for server-side rendering of web components instead.** :warning:
+
+
 ![Happy DOM Logo](https://github.com/capricorn86/happy-dom/raw/master/docs/happy-dom-logo.jpg)
 
 
 # About
 
-A [JSDOM](https://github.com/jsdom/jsdom) alternative with focus on performance.
+[Happy DOM](https://github.com/capricorn86/happy-dom) is a JavaScript implementation of a web browser without its graphical user interface. It includes many web standards from WHATWG [DOM](https://dom.spec.whatwg.org/) and [HTML](https://html.spec.whatwg.org/multipage/).
 
-[Happy DOM](https://github.com/capricorn86/happy-dom) aim to support the most common functionality of a web browser.
+The goal of [Happy DOM](https://github.com/capricorn86/happy-dom) is to emulate enough of a web browser to be useful for testing, scraping web sites and server-side rendering.
+
+[Happy DOM](https://github.com/capricorn86/happy-dom) focuses heavily on performance and can be used as an alternative to [JSDOM](https://github.com/jsdom/jsdom).
 
 This package makes it easier to setup servering side rendering of web components by handling the setup of the Node [VM Context](https://nodejs.org/api/vm.html#vm_vm_createcontext_sandbox_options) for you.
 
@@ -17,6 +22,8 @@ This package makes it easier to setup servering side rendering of web components
 
 - Shadow Root (Shadow DOM)
 
+- Declarative Shadow DOM
+
 - Mutation Observer
 
 - Tree Walker
diff --git a/packages/server-rendering/package.json b/packages/server-rendering/package.json
index 91afa41e..694fdd64 100644
--- a/packages/server-rendering/package.json
+++ b/packages/server-rendering/package.json
@@ -7,6 +7,7 @@
 	"author": "David Ortner",
 	"description": "Server side rendering of web components with happy-dom.",
 	"main": "lib/index.js",
+	"deprecated": "This package is deprecated. Happy DOM now supports Declarative Shadow DOM which can be used for server-side rendering of web components instead.",
 	"keywords": [
 		"jsdom",
 		"dom",


Test patch:
diff --git a/packages/happy-dom/test/CustomElement.ts b/packages/happy-dom/test/CustomElement.ts
index fb4e8faa..9dc096c0 100644
--- a/packages/happy-dom/test/CustomElement.ts
+++ b/packages/happy-dom/test/CustomElement.ts
@@ -1,11 +1,14 @@
 import Window from '../src/window/Window';
+import IShadowRoot from '../src/nodes/shadow-root/IShadowRoot';
 
 /**
  * CustomElement test class.
  */
 export default class CustomElement extends new Window().HTMLElement {
 	public static observedAttributesCallCount = 0;
+	public static shadowRootMode = 'open';
 	public changedAttributes = [];
+	private internalShadowRoot: IShadowRoot = null;
 
 	/**
 	 * Returns a list of observed attributes.
@@ -22,7 +25,7 @@ export default class CustomElement extends new Window().HTMLElement {
 	 */
 	constructor() {
 		super();
-		this.attachShadow({ mode: 'open' });
+		this.internalShadowRoot = this.attachShadow({ mode: CustomElement.shadowRootMode });
 	}
 
 	/**
@@ -36,7 +39,7 @@ export default class CustomElement extends new Window().HTMLElement {
 	 * @override
 	 */
 	public connectedCallback(): void {
-		this.shadowRoot.innerHTML = `
+		this.internalShadowRoot.innerHTML = `
             <style>
                 :host {
                     display: block;
@@ -59,8 +62,11 @@ export default class CustomElement extends new Window().HTMLElement {
             </style>
             <div>
                 <span>
-                    Some text.
+                    key1 is "${this.getAttribute('key1')}" and key2 is "${this.getAttribute(
+			'key2'
+		)}".
                 </span>
+                <span><slot></slot></span>
             </div>
         `;
 	}
diff --git a/packages/happy-dom/test/nodes/element/Element.test.ts b/packages/happy-dom/test/nodes/element/Element.test.ts
index f9b388db..6f7ae5e4 100644
--- a/packages/happy-dom/test/nodes/element/Element.test.ts
+++ b/packages/happy-dom/test/nodes/element/Element.test.ts
@@ -1,10 +1,10 @@
 import Window from '../../../src/window/Window';
+import IWindow from '../../../src/window/IWindow';
 import XMLSerializer from '../../../src/xml-serializer/XMLSerializer';
 import XMLParser from '../../../src/xml-parser/XMLParser';
 import CustomElement from '../../CustomElement';
 import ShadowRoot from '../../../src/nodes/shadow-root/ShadowRoot';
-import Document from '../../../src/nodes/document/Document';
-import HTMLElement from '../../../src/nodes/html-element/HTMLElement';
+import IDocument from '../../../src/nodes/document/IDocument';
 import Text from '../../../src/nodes/text/Text';
 import DOMRect from '../../../src/nodes/element/DOMRect';
 import Range from '../../../src/nodes/element/Range';
@@ -22,14 +22,14 @@ import INodeList from '../../../src/nodes/node/INodeList';
 const NAMESPACE_URI = 'https://test.test';
 
 describe('Element', () => {
-	let window: Window;
-	let document: Document;
-	let element: HTMLElement;
+	let window: IWindow;
+	let document: IDocument;
+	let element: IElement;
 
 	beforeEach(() => {
 		window = new Window();
 		document = window.document;
-		element = <HTMLElement>document.createElement('div');
+		element = <IElement>document.createElement('div');
 		window.customElements.define('custom-element', CustomElement);
 	});
 
@@ -346,6 +346,37 @@ describe('Element', () => {
 		});
 	});
 
+	describe('getInnerHTML()', () => {
+		it('Returns HTML of children as a concatenated string.', () => {
+			const div = document.createElement('div');
+
+			element.appendChild(div);
+
+			jest.spyOn(XMLSerializer.prototype, 'serializeToString').mockImplementation(rootElement => {
+				expect(rootElement).toBe(div);
+				return 'EXPECTED_HTML';
+			});
+
+			expect(element.getInnerHTML()).toBe('EXPECTED_HTML');
+		});
+
+		it('Returns HTML of children and shadow roots of custom elements as a concatenated string.', () => {
+			const div = document.createElement('div');
+
+			element.appendChild(div);
+
+			jest
+				.spyOn(XMLSerializer.prototype, 'serializeToString')
+				.mockImplementation((rootElement, options) => {
+					expect(rootElement).toBe(div);
+					expect(options).toEqual({ includeShadowRoots: true });
+					return 'EXPECTED_HTML';
+				});
+
+			expect(element.getInnerHTML({ includeShadowRoots: true })).toBe('EXPECTED_HTML');
+		});
+	});
+
 	describe('append()', () => {
 		it('Inserts a set of Node objects or DOMString objects after the last child of the ParentNode. DOMString objects are inserted as equivalent Text nodes.', () => {
 			const node1 = document.createComment('test1');
@@ -1153,14 +1184,25 @@ describe('Element', () => {
 	});
 
 	describe('attachShadow()', () => {
-		it('Creates a new ShadowRoot node and sets it to the shadowRoot property.', () => {
+		it('Creates a new open ShadowRoot node and sets it to the "shadowRoot" property.', () => {
 			element.attachShadow({ mode: 'open' });
+			expect(element['_shadowRoot'] instanceof ShadowRoot).toBe(true);
 			expect(element.shadowRoot instanceof ShadowRoot).toBe(true);
 			expect(element.shadowRoot.ownerDocument).toBe(document);
 			expect(element.shadowRoot.isConnected).toBe(false);
 			document.appendChild(element);
 			expect(element.shadowRoot.isConnected).toBe(true);
 		});
+
+		it('Creates a new closed ShadowRoot node and sets it to the internal "_shadowRoot" property.', () => {
+			element.attachShadow({ mode: 'closed' });
+			expect(element.shadowRoot).toBe(null);
+			expect(element['_shadowRoot'] instanceof ShadowRoot).toBe(true);
+			expect(element['_shadowRoot'].ownerDocument).toBe(document);
+			expect(element['_shadowRoot'].isConnected).toBe(false);
+			document.appendChild(element);
+			expect(element['_shadowRoot'].isConnected).toBe(true);
+		});
 	});
 
 	for (const functionName of ['scroll', 'scrollTo']) {
@@ -1234,7 +1276,7 @@ describe('Element', () => {
 
 			child.className = 'className';
 
-			element.tagName = 'tagName';
+			(<string>element.tagName) = 'tagName';
 			(<number>element.scrollLeft) = 10;
 			(<number>element.scrollTop) = 10;
 
diff --git a/packages/happy-dom/test/xml-serializer/XMLSerializer.test.ts b/packages/happy-dom/test/xml-serializer/XMLSerializer.test.ts
index 94526f67..9946cbbf 100644
--- a/packages/happy-dom/test/xml-serializer/XMLSerializer.test.ts
+++ b/packages/happy-dom/test/xml-serializer/XMLSerializer.test.ts
@@ -1,6 +1,7 @@
 import XMLSerializer from '../../src/xml-serializer/XMLSerializer';
 import Window from '../../src/window/Window';
 import Document from '../../src/nodes/document/Document';
+import CustomElement from '../CustomElement';
 
 describe('XMLSerializer', () => {
 	let window: Window;
@@ -11,6 +12,12 @@ describe('XMLSerializer', () => {
 		window = new Window();
 		document = window.document;
 		xmlSerializer = new XMLSerializer();
+
+		window.customElements.define('custom-element', CustomElement);
+	});
+
+	afterEach(() => {
+		CustomElement.shadowRootMode = 'open';
 	});
 
 	describe('serializeToString()', () => {
@@ -83,7 +90,7 @@ describe('XMLSerializer', () => {
 			);
 		});
 
-		it('Serializes a closed custom element.', () => {
+		it('Serializes a custom element.', () => {
 			const div = document.createElement('div');
 			const customElement = document.createElement('custom-element');
 
@@ -101,6 +108,68 @@ describe('XMLSerializer', () => {
 			);
 		});
 
+		it('Includes shadow roots of custom elements when the "includeShadowRoots" sent in as an option.', () => {
+			const div = document.createElement('div');
+			const customElement1 = document.createElement('custom-element');
+
+			CustomElement.shadowRootMode = 'closed';
+
+			const customElement2 = document.createElement('custom-element');
+
+			customElement1.setAttribute('key1', 'value1');
+			customElement1.setAttribute('key2', 'value2');
+			customElement1.innerHTML = '<span>Slotted content</span>';
+
+			customElement2.setAttribute('key1', 'value4');
+			customElement2.setAttribute('key2', 'value5');
+
+			div.appendChild(customElement1);
+			div.appendChild(customElement2);
+
+			// Connects the custom element to DOM which will trigger connectedCallback() on it
+			document.body.appendChild(div);
+
+			expect(
+				xmlSerializer.serializeToString(div, { includeShadowRoots: true }).replace(/[\s]/gm, '')
+			).toBe(
+				`
+					<div>
+						<custom-element key1="value1" key2="value2">
+							<span>Slotted content</span>
+							<template shadowroot="open">
+								<style>
+									:host {
+										display: block;
+									}
+									div {
+										color: red;
+									}
+									.class1 {
+										color: blue;
+									}
+									.class1.class2 span {
+										color: green;
+									}
+									.class1[attr1="value1"] {
+										color: yellow;
+									}
+									[attr1="value1"] {
+										color: yellow;
+									}
+								</style>
+								<div>
+									<span>
+										key1 is "value1" and key2 is "value2".
+									</span>
+									<span><slot></slot></span>
+								</div>
+							</template>
+						</custom-element>
+						<custom-element key1="value4" key2="value5"></custom-element>
+					</div>`.replace(/[\s]/gm, '')
+			);
+		});
+
 		it('Does not escape unicode attributes.', () => {
 			const div = document.createElement('div');
 
diff --git a/packages/server-rendering/test/context/mock-data/HappyDOMContextScript.js b/packages/server-rendering/test/context/mock-data/HappyDOMContextScript.js
index 756b4195..103db850 100644
--- a/packages/server-rendering/test/context/mock-data/HappyDOMContextScript.js
+++ b/packages/server-rendering/test/context/mock-data/HappyDOMContextScript.js
@@ -8,7 +8,7 @@ class CustomElement extends HTMLElement {
 	constructor() {
         super();
         this.changedAttributes = [];
-		this.attachShadow({ mode: 'closed' });
+		this.attachShadow({ mode: 'open' });
 	}
 
 	attributeChangedCallback(name, oldValue, newValue) {