Repository: capricorn86/happy-dom
Pull request #270 (2021-05-06 15:24:31)

#249@minor: Adds support for Node.getRootNode().


Patch:
diff --git a/packages/happy-dom/src/nodes/node/INode.ts b/packages/happy-dom/src/nodes/node/INode.ts
index 7a3a3dc6..566c3d85 100644
--- a/packages/happy-dom/src/nodes/node/INode.ts
+++ b/packages/happy-dom/src/nodes/node/INode.ts
@@ -18,6 +18,7 @@ export default interface INode extends IEventTarget {
 	textContent: string;
 	connectedCallback?(): void;
 	disconnectedCallback?(): void;
+	getRootNode(options?: { composed: boolean }): INode;
 	cloneNode(deep: boolean): INode;
 	appendChild(node: INode): INode;
 	removeChild(node: INode): INode;
diff --git a/packages/happy-dom/src/nodes/node/Node.ts b/packages/happy-dom/src/nodes/node/Node.ts
index 2c2b8511..4bd0613b 100644
--- a/packages/happy-dom/src/nodes/node/Node.ts
+++ b/packages/happy-dom/src/nodes/node/Node.ts
@@ -9,6 +9,7 @@ import IDocument from '../document/IDocument';
 import IElement from '../element/IElement';
 import INodeList from './INodeList';
 import NodeListFactory from './NodeListFactory';
+import { IShadowRoot } from '../..';
 
 /**
  * Node
@@ -190,6 +191,31 @@ export default class Node extends EventTarget implements INode {
 	 */
 	public disconnectedCallback?(): void;
 
+	/**
+	 * Returns closest root node (Document or ShadowRoot).
+	 *
+	 * @param options Options.
+	 * @param options.composed A Boolean that indicates whether the shadow root should be returned (false, the default), or a root node beyond shadow root (true).
+	 * @returns Node.
+	 */
+	public getRootNode(options?: { composed: boolean }): INode {
+		// eslint-disable-next-line
+        let parent: INode = this;
+
+		while (!!parent) {
+			if (!parent.parentNode) {
+				if (!options?.composed || !(<IShadowRoot>parent).host) {
+					return parent;
+				}
+				parent = (<IShadowRoot>parent).host;
+			} else {
+				parent = parent.parentNode;
+			}
+		}
+
+		return null;
+	}
+
 	/**
 	 * Clones a node.
 	 *


Test patch:
diff --git a/packages/happy-dom/test/nodes/node/Node.test.ts b/packages/happy-dom/test/nodes/node/Node.test.ts
index 4bd47f2a..66f1fb9a 100644
--- a/packages/happy-dom/test/nodes/node/Node.test.ts
+++ b/packages/happy-dom/test/nodes/node/Node.test.ts
@@ -6,10 +6,19 @@ import Event from '../../../src/event/Event';
 class CustomCounterElement extends HTMLElement {
 	public static output = [];
 
+	/**
+	 * Constructor.
+	 */
+	constructor() {
+		super();
+		this.attachShadow({ mode: 'open' });
+	}
+
 	/**
 	 * Connected
 	 */
 	public connectedCallback(): void {
+		this.shadowRoot.innerHTML = '<div><span>Test</span></div>';
 		(<typeof CustomCounterElement>this.constructor).output.push('Counter:connected');
 	}
 
@@ -223,6 +232,42 @@ describe('Node', () => {
 		});
 	});
 
+	describe('getRootNode()', () => {
+		test('Returns ShadowRoot when used on a node inside a ShadowRoot.', () => {
+			const customElement = document.createElement('custom-counter');
+
+			document.body.appendChild(customElement);
+
+			const rootNode = customElement.shadowRoot.querySelector('span').getRootNode();
+
+			expect(rootNode).toBe(customElement.shadowRoot);
+		});
+
+		test('Returns Document when used on a node inside a ShadowRoot and the option "composed" is set to "true".', () => {
+			const customElement = document.createElement('custom-counter');
+
+			document.body.appendChild(customElement);
+
+			const rootNode = customElement.shadowRoot
+				.querySelector('span')
+				.getRootNode({ composed: true });
+
+			expect(rootNode).toBe(document);
+		});
+
+		test('Returns Document when the node is not inside a ShadowRoot.', () => {
+			const divElement = document.createElement('div');
+			const spanElement = document.createElement('span');
+
+			divElement.appendChild(spanElement);
+			document.body.appendChild(divElement);
+
+			const rootNode = spanElement.getRootNode();
+
+			expect(rootNode).toBe(document);
+		});
+	});
+
 	describe('cloneNode()', () => {
 		test('Makes a shallow clone of a node (default behavior).', () => {
 			const div = document.createElement('div');