Repository: capricorn86/happy-dom (ts)

Issue:
n.compareDocumentPosition is not a function
Hi there 👋🏻 ,

I hope it is the right place to ask, and I apologize if not. I'm currently using `happy-dom` with `vitest` but I'm facing the following error:

![image](https://user-images.githubusercontent.com/11817371/176379353-d55400e2-ca84-4189-82a4-6bc6831b8627.png)

```text
Binaries:
    Node: 16.15.1 - /usr/bin/node
    npm: 8.11.0 - /usr/bin/npm
  npmPackages:
    happy-dom: 5.3.4 => 5.3.4 
    vite: 2.9.13 => 2.9.13 
    vitest: 0.16.0 => 0.16.0
```

Reproduction repo: [smarlhens/vite-vitest-happy-dom-compare-document-position-is-not-a-function](https://github.com/smarlhens/vite-vitest-happy-dom-compare-document-position-is-not-a-function)

You can check using `jsdom` with `vitest` by switching `test.environment: 'jsdom'` in `vite.config.ts`.


Thanks in advance for your help, wish you a nice day sunny

Pull request:
#524@minor: Add support for Node.compareDocumentPosition().
Resolves #524 

Hello there 👋🏻 

I've tried to implement `compareDocumentPosition` into the code base. I hope I've done the job correctly. Feel free to send me some feedback if needed 🙏🏻 I'll be happy to learn from you guys.

Notable changes:
- Added `nodeType` in `Attr` (of type `ATTRIBUTE_NODE`)
- Added `ProcessingInstruction` node & `IProcessingInstruction` interface
- Added `createProcessingInstruction` in `Document` (required to properly test `nodeEquals` method)
- Added `NodeDocumentPositionEnum` enum

References:
- https://dom.spec.whatwg.org/#dom-node-comparedocumentposition
- https://dom.spec.whatwg.org/#attr
- https://dom.spec.whatwg.org/#dom-node-attribute_node
- https://dom.spec.whatwg.org/#concept-node-equals
- https://dom.spec.whatwg.org/#processinginstruction
- https://dom.spec.whatwg.org/#dom-document-createprocessinginstruction
- https://github.com/jsdom/jsdom

Patch:
diff --git a/packages/happy-dom/src/index.ts b/packages/happy-dom/src/index.ts
index 6caa9b43..e11bc7ad 100644
--- a/packages/happy-dom/src/index.ts
+++ b/packages/happy-dom/src/index.ts
@@ -120,6 +120,10 @@ import Selection from './selection/Selection';
 import Range from './range/Range';
 import HTMLDialogElement from './nodes/html-dialog-element/HTMLDialogElement';
 import IHTMLDialogElement from './nodes/html-dialog-element/IHTMLDialogElement';
+import Attr from './nodes/attr/Attr';
+import IAttr from './nodes/attr/IAttr';
+import ProcessingInstruction from './nodes/processing-instruction/ProcessingInstruction';
+import IProcessingInstruction from './nodes/processing-instruction/IProcessingInstruction';
 
 export {
 	GlobalWindow,
@@ -243,5 +247,9 @@ export {
 	Selection,
 	Range,
 	HTMLDialogElement,
-	IHTMLDialogElement
+	IHTMLDialogElement,
+	Attr,
+	IAttr,
+	ProcessingInstruction,
+	IProcessingInstruction
 };
diff --git a/packages/happy-dom/src/nodes/attr/Attr.ts b/packages/happy-dom/src/nodes/attr/Attr.ts
index 6a881412..72427082 100644
--- a/packages/happy-dom/src/nodes/attr/Attr.ts
+++ b/packages/happy-dom/src/nodes/attr/Attr.ts
@@ -8,6 +8,7 @@ import IAttr from './IAttr';
  * Reference: https://developer.mozilla.org/en-US/docs/Web/API/Attr.
  */
 export default class Attr extends Node implements IAttr {
+	public readonly nodeType = Node.ATTRIBUTE_NODE;
 	public value: string = null;
 	public name: string = null;
 	public namespaceURI: string = null;
diff --git a/packages/happy-dom/src/nodes/document/Document.ts b/packages/happy-dom/src/nodes/document/Document.ts
index 02f449f6..142ca876 100644
--- a/packages/happy-dom/src/nodes/document/Document.ts
+++ b/packages/happy-dom/src/nodes/document/Document.ts
@@ -41,6 +41,10 @@ import IShadowRoot from '../shadow-root/IShadowRoot';
 import Range from '../../range/Range';
 import IHTMLBaseElement from '../html-base-element/IHTMLBaseElement';
 import IAttr from '../attr/IAttr';
+import IProcessingInstruction from '../processing-instruction/IProcessingInstruction';
+import ProcessingInstruction from '../processing-instruction/ProcessingInstruction';
+
+const PROCESSING_INSTRUCTION_TARGET_REGEXP = /^[a-z][a-z0-9-]+$/;
 
 /**
  * Document.
@@ -824,4 +828,28 @@ export default class Document extends Node implements IDocument {
 			this.dispatchEvent(new Event('load', { bubbles: true }));
 		});
 	}
+
+	/**
+	 * Creates a Processing Instruction node.
+	 *
+	 * @returns IProcessingInstruction.
+	 * @param target
+	 * @param data
+	 */
+	public createProcessingInstruction(target: string, data: string): IProcessingInstruction {
+		if (!target || !PROCESSING_INSTRUCTION_TARGET_REGEXP.test(target)) {
+			throw new DOMException(
+				`Failed to execute 'createProcessingInstruction' on 'Document': The target provided ('${target}') is not a valid name.`
+			);
+		}
+		if (data.includes('?>')) {
+			throw new DOMException(
+				`Failed to execute 'createProcessingInstruction' on 'Document': The data provided ('?>') contains '?>'`
+			);
+		}
+		ProcessingInstruction._ownerDocument = this;
+		const processingInstruction = new ProcessingInstruction(data);
+		processingInstruction.target = target;
+		return processingInstruction;
+	}
 }
diff --git a/packages/happy-dom/src/nodes/document/IDocument.ts b/packages/happy-dom/src/nodes/document/IDocument.ts
index bb5f6059..5e219728 100644
--- a/packages/happy-dom/src/nodes/document/IDocument.ts
+++ b/packages/happy-dom/src/nodes/document/IDocument.ts
@@ -19,6 +19,7 @@ import Location from '../../location/Location';
 import DocumentReadyStateEnum from './DocumentReadyStateEnum';
 import INodeList from '../node/INodeList';
 import Range from '../../range/Range';
+import IProcessingInstruction from '../processing-instruction/IProcessingInstruction';
 
 /**
  * Document.
@@ -197,4 +198,13 @@ export default interface IDocument extends IParentNode {
 	 * @returns "true" if the document has focus.
 	 */
 	hasFocus(): boolean;
+
+	/**
+	 * Creates a Processing Instruction node.
+	 *
+	 * @returns IProcessingInstruction.
+	 * @param target
+	 * @param data
+	 */
+	createProcessingInstruction(target: string, data: string): IProcessingInstruction;
 }
diff --git a/packages/happy-dom/src/nodes/element/Element.ts b/packages/happy-dom/src/nodes/element/Element.ts
index 0d380934..ae16d819 100644
--- a/packages/happy-dom/src/nodes/element/Element.ts
+++ b/packages/happy-dom/src/nodes/element/Element.ts
@@ -39,6 +39,7 @@ export default class Element extends Node implements IElement {
 	public tagName: string = null;
 	public nodeType = Node.ELEMENT_NODE;
 	public shadowRoot: IShadowRoot = null;
+	public prefix: string = null;
 
 	public scrollTop = 0;
 	public scrollLeft = 0;
diff --git a/packages/happy-dom/src/nodes/element/IElement.ts b/packages/happy-dom/src/nodes/element/IElement.ts
index cdbb4137..9d72b9e9 100644
--- a/packages/happy-dom/src/nodes/element/IElement.ts
+++ b/packages/happy-dom/src/nodes/element/IElement.ts
@@ -18,6 +18,7 @@ export default interface IElement extends IChildNode, INonDocumentTypeChildNode,
 	readonly shadowRoot: IShadowRoot;
 	readonly classList: IDOMTokenList;
 	readonly namespaceURI: string;
+	prefix: string | null;
 	scrollTop: number;
 	scrollLeft: number;
 	id: string;
diff --git a/packages/happy-dom/src/nodes/node/INode.ts b/packages/happy-dom/src/nodes/node/INode.ts
index 54d4afbd..f0c2c314 100644
--- a/packages/happy-dom/src/nodes/node/INode.ts
+++ b/packages/happy-dom/src/nodes/node/INode.ts
@@ -3,6 +3,7 @@ import IDocument from '../document/IDocument';
 import IElement from '../element/IElement';
 import INodeList from './INodeList';
 import NodeTypeEnum from './NodeTypeEnum';
+import NodeDocumentPositionEnum from './NodeDocumentPositionEnum';
 
 export default interface INode extends IEventTarget {
 	readonly ELEMENT_NODE: NodeTypeEnum;
@@ -14,6 +15,12 @@ export default interface INode extends IEventTarget {
 	readonly DOCUMENT_TYPE_NODE: NodeTypeEnum;
 	readonly DOCUMENT_FRAGMENT_NODE: NodeTypeEnum;
 	readonly PROCESSING_INSTRUCTION_NODE: NodeTypeEnum;
+	readonly DOCUMENT_POSITION_DISCONNECTED: NodeDocumentPositionEnum;
+	readonly DOCUMENT_POSITION_PRECEDING: NodeDocumentPositionEnum;
+	readonly DOCUMENT_POSITION_FOLLOWING: NodeDocumentPositionEnum;
+	readonly DOCUMENT_POSITION_CONTAINS: NodeDocumentPositionEnum;
+	readonly DOCUMENT_POSITION_CONTAINED_BY: NodeDocumentPositionEnum;
+	readonly DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC: NodeDocumentPositionEnum;
 	readonly ownerDocument: IDocument;
 	readonly parentNode: INode;
 	readonly parentElement: IElement;
@@ -39,4 +46,5 @@ export default interface INode extends IEventTarget {
 	insertBefore(newNode: INode, referenceNode?: INode | null): INode;
 	replaceChild(newChild: INode, oldChild: INode): INode;
 	toString(): string;
+	compareDocumentPosition(otherNode: INode): number;
 }
diff --git a/packages/happy-dom/src/nodes/node/Node.ts b/packages/happy-dom/src/nodes/node/Node.ts
index 17bd330f..35799a2d 100644
--- a/packages/happy-dom/src/nodes/node/Node.ts
+++ b/packages/happy-dom/src/nodes/node/Node.ts
@@ -11,6 +11,9 @@ import IHTMLBaseElement from '../html-base-element/IHTMLBaseElement';
 import INodeList from './INodeList';
 import NodeListFactory from './NodeListFactory';
 import NodeTypeEnum from './NodeTypeEnum';
+import NodeDocumentPositionEnum from './NodeDocumentPositionEnum';
+import NodeUtility from './NodeUtility';
+import IAttr from '../attr/IAttr';
 
 /**
  * Node.
@@ -29,6 +32,13 @@ export default class Node extends EventTarget implements INode {
 	public static readonly DOCUMENT_TYPE_NODE = NodeTypeEnum.documentTypeNode;
 	public static readonly DOCUMENT_FRAGMENT_NODE = NodeTypeEnum.documentFragmentNode;
 	public static readonly PROCESSING_INSTRUCTION_NODE = NodeTypeEnum.processingInstructionNode;
+	public static readonly DOCUMENT_POSITION_CONTAINED_BY = NodeDocumentPositionEnum.containedBy;
+	public static readonly DOCUMENT_POSITION_CONTAINS = NodeDocumentPositionEnum.contains;
+	public static readonly DOCUMENT_POSITION_DISCONNECTED = NodeDocumentPositionEnum.disconnect;
+	public static readonly DOCUMENT_POSITION_FOLLOWING = NodeDocumentPositionEnum.following;
+	public static readonly DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC =
+		NodeDocumentPositionEnum.implementationSpecific;
+	public static readonly DOCUMENT_POSITION_PRECEDING = NodeDocumentPositionEnum.preceding;
 	public readonly ELEMENT_NODE = NodeTypeEnum.elementNode;
 	public readonly ATTRIBUTE_NODE = NodeTypeEnum.attributeNode;
 	public readonly TEXT_NODE = NodeTypeEnum.textNode;
@@ -38,6 +48,13 @@ export default class Node extends EventTarget implements INode {
 	public readonly DOCUMENT_TYPE_NODE = NodeTypeEnum.documentTypeNode;
 	public readonly DOCUMENT_FRAGMENT_NODE = NodeTypeEnum.documentFragmentNode;
 	public readonly PROCESSING_INSTRUCTION_NODE = NodeTypeEnum.processingInstructionNode;
+	public readonly DOCUMENT_POSITION_CONTAINED_BY = NodeDocumentPositionEnum.containedBy;
+	public readonly DOCUMENT_POSITION_CONTAINS = NodeDocumentPositionEnum.contains;
+	public readonly DOCUMENT_POSITION_DISCONNECTED = NodeDocumentPositionEnum.disconnect;
+	public readonly DOCUMENT_POSITION_FOLLOWING = NodeDocumentPositionEnum.following;
+	public readonly DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC =
+		NodeDocumentPositionEnum.implementationSpecific;
+	public readonly DOCUMENT_POSITION_PRECEDING = NodeDocumentPositionEnum.preceding;
 	public readonly ownerDocument: IDocument = null;
 	public readonly parentNode: INode = null;
 	public readonly nodeType: number;
@@ -444,7 +461,7 @@ export default class Node extends EventTarget implements INode {
 			}
 
 			// eslint-disable-next-line
-			if(event.composed && (<any>this).host) {
+			if (event.composed && (<any>this).host) {
 				// eslint-disable-next-line
 				return (<any>this).host.dispatchEvent(event);
 			}
@@ -528,4 +545,177 @@ export default class Node extends EventTarget implements INode {
 			}
 		}
 	}
+
+	/**
+	 * Reports the position of its argument node relative to the node on which it is called.
+	 *
+	 * @see https://dom.spec.whatwg.org/#dom-node-comparedocumentposition
+	 * @param otherNode Other node.
+	 */
+	public compareDocumentPosition(otherNode: INode): number {
+		/**
+		 * 1. If this is other, then return zero.
+		 */
+		if (this === otherNode) {
+			return 0;
+		}
+
+		/**
+		 * 2. Let node1 be other and node2 be this.
+		 */
+		let node1: INode = otherNode;
+		let node2: INode = this;
+
+		/**
+		 * 3. Let attr1 and attr2 be null.
+		 */
+		let attr1 = null;
+		let attr2 = null;
+
+		/**
+		 * 4. If node1 is an attribute, then set attr1 to node1 and node1 to attr1’s element.
+		 */
+		if (node1.nodeType === Node.ATTRIBUTE_NODE) {
+			attr1 = node1;
+			node1 = (<IAttr>attr1).ownerElement;
+		}
+
+		/**
+		 * 5. If node2 is an attribute, then:
+		 * 5.1. Set attr2 to node2 and node2 to attr2’s element.
+		 */
+		if (node2.nodeType === Node.ATTRIBUTE_NODE) {
+			attr2 = node2;
+			node2 = (<IAttr>attr2).ownerElement;
+
+			/**
+			 * 5.2. If attr1 and node1 are non-null, and node2 is node1, then:
+			 */
+			if (attr1 !== null && node1 !== null && node2 === node1) {
+				/**
+				 * 5.2.1. For each attr in node2’s attribute list:
+				 */
+				for (const attr of Object.values((<IElement>node2).attributes)) {
+					/**
+					 * 5.2.1.1. If attr equals attr1, then return the result of adding DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC and DOCUMENT_POSITION_PRECEDING.
+					 */
+					if (NodeUtility.nodeEquals(<IAttr>attr, attr1)) {
+						return (
+							Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC | Node.DOCUMENT_POSITION_PRECEDING
+						);
+					}
+
+					/**
+					 * 5.2.1.2. If attr equals attr2, then return the result of adding DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC and DOCUMENT_POSITION_FOLLOWING.
+					 */
+					if (NodeUtility.nodeEquals(<IAttr>attr, attr2)) {
+						return (
+							Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC | Node.DOCUMENT_POSITION_FOLLOWING
+						);
+					}
+				}
+			}
+		}
+
+		const node2Ancestors: INode[] = [];
+		let node2Ancestor: INode = node2;
+
+		while (node2Ancestor) {
+			/**
+			 * 7. If node1 is an ancestor of node2 […] then return the result of adding DOCUMENT_POSITION_CONTAINS to DOCUMENT_POSITION_PRECEDING.
+			 */
+			if (node2Ancestor === node1) {
+				return Node.DOCUMENT_POSITION_CONTAINS | Node.DOCUMENT_POSITION_PRECEDING;
+			}
+
+			node2Ancestors.push(node2Ancestor);
+			node2Ancestor = node2Ancestor.parentNode;
+		}
+
+		const node1Ancestors: INode[] = [];
+		let node1Ancestor: INode = node1;
+
+		while (node1Ancestor) {
+			/**
+			 * 8. If node1 is a descendant of node2 […] then return the result of adding DOCUMENT_POSITION_CONTAINED_BY to DOCUMENT_POSITION_FOLLOWING.
+			 */
+			if (node1Ancestor === node2) {
+				return Node.DOCUMENT_POSITION_CONTAINED_BY | Node.DOCUMENT_POSITION_FOLLOWING;
+			}
+
+			node1Ancestors.push(node1Ancestor);
+			node1Ancestor = node1Ancestor.parentNode;
+		}
+
+		const reverseArrayIndex = (array: INode[], reverseIndex: number): INode => {
+			return array[array.length - 1 - reverseIndex];
+		};
+
+		const root = reverseArrayIndex(node2Ancestors, 0);
+
+		/**
+		 * 6. If node1 or node2 is null, or node1’s root is not node2’s root, then return the result of adding
+		 * DOCUMENT_POSITION_DISCONNECTED, DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC, and either
+		 * DOCUMENT_POSITION_PRECEDING or DOCUMENT_POSITION_FOLLOWING, with the constraint that this is to be consistent, together.
+		 */
+		if (!root || root !== reverseArrayIndex(node1Ancestors, 0)) {
+			return (
+				Node.DOCUMENT_POSITION_DISCONNECTED |
+				Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC |
+				Node.DOCUMENT_POSITION_FOLLOWING
+			);
+		}
+
+		// Find the lowest common ancestor
+		let commonAncestorIndex = 0;
+		const ancestorsMinLength = Math.min(node2Ancestors.length, node1Ancestors.length);
+
+		for (let i = 0; i < ancestorsMinLength; ++i) {
+			const node2Ancestor = reverseArrayIndex(node2Ancestors, i);
+			const node1Ancestor = reverseArrayIndex(node1Ancestors, i);
+
+			if (node2Ancestor !== node1Ancestor) {
+				break;
+			}
+
+			commonAncestorIndex = i;
+		}
+
+		const commonAncestor = reverseArrayIndex(node2Ancestors, commonAncestorIndex);
+
+		// Indexes within the common ancestor
+		let indexes = 0;
+		let node2Index = -1;
+		let node1Index = -1;
+		const node2Node = reverseArrayIndex(node2Ancestors, commonAncestorIndex + 1);
+		const node1Node = reverseArrayIndex(node1Ancestors, commonAncestorIndex + 1);
+
+		const computeNodeIndexes = (nodes: INode[]): void => {
+			for (const childNode of nodes) {
+				computeNodeIndexes(childNode.childNodes);
+
+				if (childNode === node2Node) {
+					node2Index = indexes;
+				} else if (childNode === node1Node) {
+					node1Index = indexes;
+				}
+
+				if (node2Index !== -1 && node1Index !== -1) {
+					break;
+				}
+
+				indexes++;
+			}
+		};
+
+		computeNodeIndexes(commonAncestor.childNodes);
+
+		/**
+		 * 9. If node1 is preceding node2, then return DOCUMENT_POSITION_PRECEDING.
+		 * 10. Return DOCUMENT_POSITION_FOLLOWING.
+		 */
+		return node1Index < node2Index
+			? Node.DOCUMENT_POSITION_PRECEDING
+			: Node.DOCUMENT_POSITION_FOLLOWING;
+	}
 }
diff --git a/packages/happy-dom/src/nodes/node/NodeDocumentPositionEnum.ts b/packages/happy-dom/src/nodes/node/NodeDocumentPositionEnum.ts
new file mode 100644
index 00000000..7c4fedfd
--- /dev/null
+++ b/packages/happy-dom/src/nodes/node/NodeDocumentPositionEnum.ts
@@ -0,0 +1,10 @@
+enum NodeDocumentPositionEnum {
+	disconnect = 0x01,
+	preceding = 0x02,
+	following = 0x04,
+	contains = 0x08,
+	containedBy = 0x10,
+	implementationSpecific = 0x20
+}
+
+export default NodeDocumentPositionEnum;
diff --git a/packages/happy-dom/src/nodes/node/NodeUtility.ts b/packages/happy-dom/src/nodes/node/NodeUtility.ts
index 93ae584c..12e91da9 100644
--- a/packages/happy-dom/src/nodes/node/NodeUtility.ts
+++ b/packages/happy-dom/src/nodes/node/NodeUtility.ts
@@ -2,6 +2,10 @@ import IText from '../text/IText';
 import IComment from '../comment/IComment';
 import INode from './INode';
 import NodeTypeEnum from './NodeTypeEnum';
+import IElement from '../element/IElement';
+import IDocumentType from '../document-type/IDocumentType';
+import IAttr from '../attr/IAttr';
+import IProcessingInstruction from '../processing-instruction/IProcessingInstruction';
 
 /**
  * Node utility.
@@ -136,4 +140,137 @@ export default class NodeUtility {
 
 		return node.nextSibling;
 	}
+
+	/**
+	 * Needed by https://dom.spec.whatwg.org/#concept-node-equals
+	 *
+	 * @param elementA
+	 * @param elementB
+	 */
+	public static attributeListsEqual(elementA: IElement, elementB: IElement): boolean {
+		const listA = Object.values(elementA.attributes);
+		const listB = Object.values(elementB.attributes);
+
+		const lengthA = listA.length;
+		const lengthB = listB.length;
+
+		if (lengthA !== lengthB) {
+			return false;
+		}
+
+		for (let i = 0; i < lengthA; ++i) {
+			const attrA = listA[i];
+
+			if (
+				!listB.some((attrB) => {
+					return (
+						(typeof attrA === 'number' && typeof attrB === 'number' && attrA === attrB) ||
+						(typeof attrA === 'object' &&
+							typeof attrB === 'object' &&
+							NodeUtility.nodeEquals(attrA, attrB))
+					);
+				})
+			) {
+				return false;
+			}
+		}
+
+		return true;
+	}
+
+	/**
+	 * Check if node nodeA equals node nodeB.
+	 * Reference: https://dom.spec.whatwg.org/#concept-node-equals
+	 *
+	 * @param nodeA Node A.
+	 * @param nodeB Node B.
+	 */
+	public static nodeEquals(nodeA: INode, nodeB: INode): boolean {
+		if (nodeA.nodeType !== nodeB.nodeType) {
+			return false;
+		}
+
+		switch (nodeA.nodeType) {
+			case NodeTypeEnum.documentTypeNode:
+				const documentTypeA = <IDocumentType>nodeA;
+				const documentTypeB = <IDocumentType>nodeB;
+
+				if (
+					documentTypeA.name !== documentTypeB.name ||
+					documentTypeA.publicId !== documentTypeB.publicId ||
+					documentTypeA.systemId !== documentTypeB.systemId
+				) {
+					return false;
+				}
+				break;
+			case NodeTypeEnum.elementNode:
+				const elementA = <IElement>nodeA;
+				const elementB = <IElement>nodeB;
+
+				if (
+					elementA.namespaceURI !== elementB.namespaceURI ||
+					elementA.prefix !== elementB.prefix ||
+					elementA.localName !== elementB.localName ||
+					elementA.attributes.length !== elementB.attributes.length
+				) {
+					return false;
+				}
+				break;
+			case NodeTypeEnum.attributeNode:
+				const attributeA = <IAttr>nodeA;
+				const attributeB = <IAttr>nodeB;
+
+				if (
+					attributeA.namespaceURI !== attributeB.namespaceURI ||
+					attributeA.localName !== attributeB.localName ||
+					attributeA.value !== attributeB.value
+				) {
+					return false;
+				}
+				break;
+			case NodeTypeEnum.processingInstructionNode:
+				const processingInstructionA = <IProcessingInstruction>nodeA;
+				const processingInstructionB = <IProcessingInstruction>nodeB;
+
+				if (
+					processingInstructionA.target !== processingInstructionB.target ||
+					processingInstructionA.data !== processingInstructionB.data
+				) {
+					return false;
+				}
+				break;
+			case NodeTypeEnum.textNode:
+			case NodeTypeEnum.commentNode:
+				type TextOrComment = IText | IComment;
+				const textOrCommentA = <TextOrComment>nodeA;
+				const textOrCommentB = <TextOrComment>nodeB;
+
+				if (textOrCommentA.data !== textOrCommentB.data) {
+					return false;
+				}
+				break;
+		}
+
+		if (
+			nodeA.nodeType === NodeTypeEnum.elementNode &&
+			!NodeUtility.attributeListsEqual(<IElement>nodeA, <IElement>nodeB)
+		) {
+			return false;
+		}
+
+		if (nodeA.childNodes.length !== nodeB.childNodes.length) {
+			return false;
+		}
+
+		for (let i = 0; i < nodeA.childNodes.length; i++) {
+			const childNodeA = nodeA.childNodes[i];
+			const childNodeB = nodeB.childNodes[i];
+
+			if (!NodeUtility.nodeEquals(childNodeA, childNodeB)) {
+				return false;
+			}
+		}
+
+		return true;
+	}
 }
diff --git a/packages/happy-dom/src/nodes/processing-instruction/IProcessingInstruction.ts b/packages/happy-dom/src/nodes/processing-instruction/IProcessingInstruction.ts
new file mode 100644
index 00000000..f609019f
--- /dev/null
+++ b/packages/happy-dom/src/nodes/processing-instruction/IProcessingInstruction.ts
@@ -0,0 +1,5 @@
+import ICharacterData from '../character-data/ICharacterData';
+
+export default interface IProcessingInstruction extends ICharacterData {
+	target: string;
+}
diff --git a/packages/happy-dom/src/nodes/processing-instruction/ProcessingInstruction.ts b/packages/happy-dom/src/nodes/processing-instruction/ProcessingInstruction.ts
new file mode 100644
index 00000000..942afd2c
--- /dev/null
+++ b/packages/happy-dom/src/nodes/processing-instruction/ProcessingInstruction.ts
@@ -0,0 +1,13 @@
+import IProcessingInstruction from './IProcessingInstruction';
+import CharacterData from '../character-data/CharacterData';
+import NodeTypeEnum from '../node/NodeTypeEnum';
+
+/**
+ * Processing instruction node interface.
+ *
+ * Reference: https://developer.mozilla.org/en-US/docs/Web/API/ProcessingInstruction.
+ */
+export default class ProcessingInstruction extends CharacterData implements IProcessingInstruction {
+	public readonly nodeType = NodeTypeEnum.processingInstructionNode;
+	public target: string;
+}
diff --git a/packages/happy-dom/src/window/IWindow.ts b/packages/happy-dom/src/window/IWindow.ts
index 43e4e27f..3d9a61eb 100644
--- a/packages/happy-dom/src/window/IWindow.ts
+++ b/packages/happy-dom/src/window/IWindow.ts
@@ -98,6 +98,7 @@ import Attr from '../nodes/attr/Attr';
 import { URLSearchParams } from 'url';
 import { Performance } from 'perf_hooks';
 import IElement from '../nodes/element/IElement';
+import ProcessingInstruction from '../nodes/processing-instruction/ProcessingInstruction';
 
 /**
  * Window without dependencies to server side specific packages.
@@ -142,6 +143,7 @@ export default interface IWindow extends IEventTarget, NodeJS.Global {
 	readonly Element: typeof Element;
 	readonly DocumentFragment: typeof DocumentFragment;
 	readonly CharacterData: typeof CharacterData;
+	readonly ProcessingInstruction: typeof ProcessingInstruction;
 	readonly NodeFilter: typeof NodeFilter;
 	readonly TreeWalker: typeof TreeWalker;
 	readonly DOMParser: typeof DOMParser;
diff --git a/packages/happy-dom/src/window/Window.ts b/packages/happy-dom/src/window/Window.ts
index bfa66362..5120800a 100644
--- a/packages/happy-dom/src/window/Window.ts
+++ b/packages/happy-dom/src/window/Window.ts
@@ -109,6 +109,7 @@ import Base64 from '../base64/Base64';
 import IDocument from '../nodes/document/IDocument';
 import Attr from '../nodes/attr/Attr';
 import IElement from '../nodes/element/IElement';
+import ProcessingInstruction from '../nodes/processing-instruction/ProcessingInstruction';
 
 const ORIGINAL_SET_TIMEOUT = setTimeout;
 const ORIGINAL_CLEAR_TIMEOUT = clearTimeout;
@@ -171,6 +172,7 @@ export default class Window extends EventTarget implements IWindow {
 	public readonly Text = Text;
 	public readonly Comment = Comment;
 	public readonly ShadowRoot = ShadowRoot;
+	public readonly ProcessingInstruction = ProcessingInstruction;
 	public readonly Element = Element;
 	public readonly DocumentFragment = DocumentFragment;
 	public readonly CharacterData = CharacterData;


Test patch:
diff --git a/packages/happy-dom/test/nodes/document/Document.test.ts b/packages/happy-dom/test/nodes/document/Document.test.ts
index 7dbde061..86f3b1b6 100644
--- a/packages/happy-dom/test/nodes/document/Document.test.ts
+++ b/packages/happy-dom/test/nodes/document/Document.test.ts
@@ -30,6 +30,8 @@ import ISVGElement from '../../../src/nodes/svg-element/ISVGElement';
 import CustomEvent from '../../../src/event/events/CustomEvent';
 import Selection from '../../../src/selection/Selection';
 import Range from '../../../src/range/Range';
+import ProcessingInstruction from '../../../src/nodes/processing-instruction/ProcessingInstruction';
+import DOMException from '../../../src/exception/DOMException';
 
 /* eslint-disable jsdoc/require-jsdoc */
 
@@ -1134,4 +1136,44 @@ describe('Document', () => {
 			expect(emittedEvent).toBe(event);
 		});
 	});
+
+	describe('createProcessingInstruction()', () => {
+		it('Creates a Processing Instruction node with target & data.', () => {
+			const instruction = document.createProcessingInstruction('foo', 'bar');
+			expect(instruction instanceof ProcessingInstruction).toBe(true);
+			expect(instruction).toEqual(
+				expect.objectContaining({
+					target: 'foo',
+					data: 'bar',
+					ownerDocument: document
+				})
+			);
+		});
+
+		it('Throws an exception if target is invalid".', () => {
+			expect.assertions(1);
+			try {
+				document.createProcessingInstruction('-foo', 'bar');
+			} catch (e) {
+				expect(e).toEqual(
+					new DOMException(
+						`Failed to execute 'createProcessingInstruction' on 'Document': The target provided ('-foo') is not a valid name.`
+					)
+				);
+			}
+		});
+
+		it('Throws an exception if data contains "?>".', () => {
+			expect.assertions(1);
+			try {
+				document.createProcessingInstruction('foo', 'bar?>');
+			} catch (e) {
+				expect(e).toEqual(
+					new DOMException(
+						`Failed to execute 'createProcessingInstruction' on 'Document': The data provided ('?>') contains '?>'`
+					)
+				);
+			}
+		});
+	});
 });
diff --git a/packages/happy-dom/test/nodes/node/Node.test.ts b/packages/happy-dom/test/nodes/node/Node.test.ts
index d31a756e..53ed11f1 100644
--- a/packages/happy-dom/test/nodes/node/Node.test.ts
+++ b/packages/happy-dom/test/nodes/node/Node.test.ts
@@ -624,4 +624,81 @@ describe('Node', () => {
 			expect(parentEvent).toBe(event);
 		});
 	});
+
+	describe('compareDocumentPosition()', () => {
+		it('Returns 0 if b is a', () => {
+			const div = document.createElement('div');
+			div.id = 'element';
+
+			document.body.appendChild(div);
+
+			expect(
+				document
+					.getElementById('element')
+					.compareDocumentPosition(document.getElementById('element'))
+			).toEqual(0);
+		});
+
+		it('Returns 4 if b is following a', () => {
+			const div = document.createElement('div');
+			const span1 = document.createElement('span');
+			span1.id = 'span1';
+			const span2 = document.createElement('span');
+			span2.id = 'span2';
+
+			div.appendChild(span1);
+			div.appendChild(span2);
+			document.body.appendChild(div);
+
+			expect(
+				document.getElementById('span1').compareDocumentPosition(document.getElementById('span2'))
+			).toEqual(4);
+		});
+
+		it('Returns 2 if b is preceding a', () => {
+			const div = document.createElement('div');
+			const span1 = document.createElement('span');
+			span1.id = 'span1';
+			const span2 = document.createElement('span');
+			span2.id = 'span2';
+
+			div.appendChild(span1);
+			div.appendChild(span2);
+			document.body.appendChild(div);
+
+			expect(
+				document.getElementById('span2').compareDocumentPosition(document.getElementById('span1'))
+			).toEqual(2);
+		});
+
+		it('Returns 20 if b is contained by a', () => {
+			const div = document.createElement('div');
+			div.id = 'parent';
+			const span = document.createElement('span');
+			span.id = 'child';
+
+			div.appendChild(span);
+			document.body.appendChild(div);
+
+			const position = document
+				.getElementById('parent')
+				.compareDocumentPosition(document.getElementById('child'));
+			expect(position).toEqual(20);
+		});
+
+		it('Returns 10 if b contains a', () => {
+			const div = document.createElement('div');
+			div.id = 'parent';
+			const span = document.createElement('span');
+			span.id = 'child';
+
+			div.appendChild(span);
+			document.body.appendChild(div);
+
+			const position = document
+				.getElementById('child')
+				.compareDocumentPosition(document.getElementById('parent'));
+			expect(position).toEqual(10);
+		});
+	});
 });
diff --git a/packages/happy-dom/test/nodes/node/NodeUtility.test.ts b/packages/happy-dom/test/nodes/node/NodeUtility.test.ts
index 17208186..305c247e 100644
--- a/packages/happy-dom/test/nodes/node/NodeUtility.test.ts
+++ b/packages/happy-dom/test/nodes/node/NodeUtility.test.ts
@@ -2,6 +2,8 @@ import Window from '../../../src/window/Window';
 import Document from '../../../src/nodes/document/Document';
 import NodeUtility from '../../../src/nodes/node/NodeUtility';
 import NodeTypeEnum from '../../../src/nodes/node/NodeTypeEnum';
+import DOMImplementation from '../../../src/dom-implementation/DOMImplementation';
+import IDocument from '../../../src/nodes/document/IDocument';
 
 describe('NodeUtility', () => {
 	let window: Window;
@@ -103,4 +105,263 @@ describe('NodeUtility', () => {
 			expect(NodeUtility.getNodeLength(div)).toBe(3);
 		});
 	});
+
+	describe('attributeListsEqual()', () => {
+		it('Returns false if lists does not have same length', () => {
+			const element1 = document.createElement('div');
+			const element2 = document.createElement('div');
+			const attrFoo = document.createAttribute('data-foo');
+			const attrBar = document.createAttribute('data-bar');
+
+			element1.setAttributeNode(attrFoo);
+			element2.setAttributeNode(attrFoo);
+			element1.setAttributeNode(attrBar);
+
+			expect(NodeUtility.attributeListsEqual(element1, element2)).toEqual(false);
+		});
+
+		it('Returns false if lists are not equal', () => {
+			const element1 = document.createElement('div');
+			const element2 = document.createElement('div');
+			const attrFoo = document.createAttribute('data-foo');
+			const attrBar = document.createAttribute('data-bar');
+			const attrLorem = document.createAttribute('data-lorem');
+
+			element1.setAttributeNode(attrFoo);
+			element1.setAttributeNode(attrBar);
+			element2.setAttributeNode(attrFoo);
+			element2.setAttributeNode(attrLorem);
+
+			expect(NodeUtility.attributeListsEqual(element1, element2)).toEqual(false);
+		});
+
+		it('Returns true if attribute lists are equal', () => {
+			const element1 = document.createElement('div');
+			const element2 = document.createElement('div');
+			const attrFoo = document.createAttribute('data-foo');
+
+			element1.setAttributeNode(attrFoo);
+			element2.setAttributeNode(attrFoo);
+
+			expect(NodeUtility.attributeListsEqual(element1, element2)).toEqual(true);
+		});
+	});
+
+	describe('nodeEquals()', () => {
+		it('Returns false if element are not of same node type', () => {
+			const element = document.createElement('div');
+			const comment = document.createComment('foo');
+
+			expect(NodeUtility.nodeEquals(element, comment)).toEqual(false);
+		});
+
+		describe('w/ document type node', () => {
+			let document: IDocument;
+			let implementation: DOMImplementation;
+
+			beforeEach(() => {
+				document = new Document();
+				implementation = new DOMImplementation(document);
+			});
+
+			it('Returns false if name are different', () => {
+				const doctype1 = implementation.createDocumentType('html1', 'foo', 'bar');
+				const doctype2 = implementation.createDocumentType('html2', 'foo', 'bar');
+
+				expect(NodeUtility.nodeEquals(doctype1, doctype2)).toEqual(false);
+			});
+
+			it('Returns false if public id are different', () => {
+				const doctype1 = implementation.createDocumentType('html', 'foo1', 'bar');
+				const doctype2 = implementation.createDocumentType('html', 'foo2', 'bar');
+
+				expect(NodeUtility.nodeEquals(doctype1, doctype2)).toEqual(false);
+			});
+
+			it('Returns false if system id are different', () => {
+				const doctype1 = implementation.createDocumentType('html', 'foo', 'bar1');
+				const doctype2 = implementation.createDocumentType('html', 'foo', 'bar2');
+
+				expect(NodeUtility.nodeEquals(doctype1, doctype2)).toEqual(false);
+			});
+
+			it('Returns true if doctype are equals', () => {
+				const doctype1 = implementation.createDocumentType('html', 'foo', 'bar');
+				const doctype2 = implementation.createDocumentType('html', 'foo', 'bar');
+
+				expect(NodeUtility.nodeEquals(doctype1, doctype2)).toEqual(true);
+			});
+		});
+
+		describe('w/ element node', () => {
+			it('Returns false if local name are different', () => {
+				const element1 = document.createElement('span');
+				const element2 = document.createElement('div');
+
+				expect(NodeUtility.nodeEquals(element1, element2)).toEqual(false);
+			});
+
+			it('Returns false if namespace URI are different', () => {
+				const element1 = document.createElementNS('ns1', 'span');
+				const element2 = document.createElementNS('ns2', 'span');
+
+				expect(NodeUtility.nodeEquals(element1, element2)).toEqual(false);
+			});
+
+			it('Returns false if prefix are different', () => {
+				const element1 = document.createElement('span');
+				const element2 = document.createElement('span');
+
+				element1.prefix = 'prefix1';
+				element2.prefix = 'prefix2';
+
+				expect(NodeUtility.nodeEquals(element1, element2)).toEqual(false);
+			});
+
+			it('Returns false if attributes list length are different', () => {
+				const element1 = document.createElement('span');
+				const element2 = document.createElement('span');
+				const attrFoo = document.createAttribute('data-foo');
+
+				element1.setAttributeNode(attrFoo);
+
+				expect(NodeUtility.nodeEquals(element1, element2)).toEqual(false);
+			});
+
+			it('Returns false if element attributes are not equal', () => {
+				const element1 = document.createElementNS('ns1', 'span');
+				const element2 = document.createElementNS('ns1', 'span');
+				element1.prefix = 'prefix1';
+				element2.prefix = 'prefix1';
+				const attrFoo = document.createAttribute('data-foo');
+				const attrBar = document.createAttribute('data-bar');
+				element1.setAttributeNode(attrFoo);
+				element2.setAttributeNode(attrBar);
+
+				expect(NodeUtility.nodeEquals(element1, element2)).toEqual(false);
+			});
+
+			it('Returns false if element does not have same amount of children', () => {
+				const element1 = document.createElement('span');
+				const element2 = document.createElement('span');
+
+				const child1 = document.createElement('span');
+				const child2 = document.createElement('span');
+				const child3 = document.createElement('span');
+
+				element1.appendChild(child1);
+				element1.appendChild(child2);
+				element2.appendChild(child3);
+
+				expect(NodeUtility.nodeEquals(element1, element2)).toEqual(false);
+			});
+
+			it("Returns false if element's children are not equals", () => {
+				const element1 = document.createElement('div');
+				const element2 = document.createElement('div');
+
+				const child1 = document.createElement('span');
+				const child2 = document.createElement('span');
+				const child3 = document.createElement('span');
+				const child4 = document.createElement('div');
+
+				element1.appendChild(child1);
+				element1.appendChild(child2);
+				element2.appendChild(child3);
+				element2.appendChild(child4);
+
+				expect(NodeUtility.nodeEquals(element1, element2)).toEqual(false);
+			});
+		});
+
+		describe('w/ attribute node', () => {
+			it('Returns false if local name are different', () => {
+				const attr1 = document.createAttribute('foo1');
+				const attr2 = document.createAttribute('foo2');
+
+				expect(NodeUtility.nodeEquals(attr1, attr2)).toEqual(false);
+			});
+
+			it('Returns false if namespace URI are different', () => {
+				const attr1 = document.createAttributeNS('ns1', 'foo');
+				const attr2 = document.createAttributeNS('ns2', 'foo');
+
+				expect(NodeUtility.nodeEquals(attr1, attr2)).toEqual(false);
+			});
+
+			it('Returns false if value are different', () => {
+				const attr1 = document.createAttribute('foo');
+				const attr2 = document.createAttribute('foo');
+
+				attr1.value = 'bar1';
+				attr2.value = 'bar2';
+
+				expect(NodeUtility.nodeEquals(attr1, attr2)).toEqual(false);
+			});
+
+			it('Returns true if elements are equal', () => {
+				const attr1 = document.createAttributeNS('ns', 'foo');
+				const attr2 = document.createAttributeNS('ns', 'foo');
+				attr1.value = 'bar';
+				attr2.value = 'bar';
+
+				expect(NodeUtility.nodeEquals(attr1, attr2)).toEqual(true);
+			});
+		});
+
+		describe('w/ processing instruction node', () => {
+			it('Returns false if target are different', () => {
+				const instruction1 = document.createProcessingInstruction('target1', 'foo');
+				const instruction2 = document.createProcessingInstruction('target2', 'foo');
+
+				expect(NodeUtility.nodeEquals(instruction1, instruction2)).toEqual(false);
+			});
+
+			it('Returns false if data are different', () => {
+				const instruction1 = document.createProcessingInstruction('target', 'foo1');
+				const instruction2 = document.createProcessingInstruction('target', 'foo2');
+
+				expect(NodeUtility.nodeEquals(instruction1, instruction2)).toEqual(false);
+			});
+
+			it('Returns true if processing instructions are equal', () => {
+				const instruction1 = document.createProcessingInstruction('target', 'foo');
+				const instruction2 = document.createProcessingInstruction('target', 'foo');
+
+				expect(NodeUtility.nodeEquals(instruction1, instruction2)).toEqual(true);
+			});
+		});
+
+		describe('w/ comment node', () => {
+			it('Returns false if data are different', () => {
+				const comment1 = document.createComment('foo1');
+				const comment2 = document.createComment('foo2');
+
+				expect(NodeUtility.nodeEquals(comment1, comment2)).toEqual(false);
+			});
+
+			it('Returns true if comments are equal', () => {
+				const comment1 = document.createComment('foo');
+				const comment2 = document.createComment('foo');
+
+				expect(NodeUtility.nodeEquals(comment1, comment2)).toEqual(true);
+			});
+		});
+
+		describe('w/ text node', () => {
+			it('Returns false if data are different', () => {
+				const textNode1 = document.createTextNode('foo1');
+				const textNode2 = document.createTextNode('foo2');
+
+				expect(NodeUtility.nodeEquals(textNode1, textNode2)).toEqual(false);
+			});
+
+			it('Returns true if text nodes are equal', () => {
+				const textNode1 = document.createTextNode('foo');
+				const textNode2 = document.createTextNode('foo');
+
+				expect(NodeUtility.nodeEquals(textNode1, textNode2)).toEqual(true);
+			});
+		});
+	});
 });