Repository: capricorn86/happy-dom
Pull request #878 (2023-04-28 16:20:55)

Task/792 refactor query selector and add support for adjacent sibling selectors


Patch:
diff --git a/packages/happy-dom/src/css/declaration/utilities/CSSStyleDeclarationElementStyle.ts b/packages/happy-dom/src/css/declaration/utilities/CSSStyleDeclarationElementStyle.ts
index 2850d158..49f1e522 100644
--- a/packages/happy-dom/src/css/declaration/utilities/CSSStyleDeclarationElementStyle.ts
+++ b/packages/happy-dom/src/css/declaration/utilities/CSSStyleDeclarationElementStyle.ts
@@ -289,8 +289,8 @@ export default class CSSStyleDeclarationElementStyle {
 						}
 					} else {
 						for (const element of options.elements) {
-							const matchResult = QuerySelector.match(element.element, selectorText);
-							if (matchResult.matches) {
+							const matchResult = QuerySelector.match(<IElement>element.element, selectorText);
+							if (matchResult) {
 								element.cssTexts.push({
 									cssText: (<CSSStyleRule>rule)._cssText,
 									priorityWeight: matchResult.priorityWeight
diff --git a/packages/happy-dom/src/nodes/element/Element.ts b/packages/happy-dom/src/nodes/element/Element.ts
index 8a34c7ea..3130329f 100644
--- a/packages/happy-dom/src/nodes/element/Element.ts
+++ b/packages/happy-dom/src/nodes/element/Element.ts
@@ -723,7 +723,7 @@ export default class Element extends Node implements IElement {
 	 * @returns "true" if matching.
 	 */
 	public matches(selector: string): boolean {
-		return QuerySelector.match(this, selector).matches;
+		return !!QuerySelector.match(this, selector);
 	}
 
 	/**
@@ -733,30 +733,16 @@ export default class Element extends Node implements IElement {
 	 * @returns Closest matching element.
 	 */
 	public closest(selector: string): IElement {
-		let rootElement: IElement = this.ownerDocument.documentElement;
-		if (!this.isConnected) {
-			rootElement = this;
-			while (rootElement.parentNode) {
-				rootElement = <IElement>rootElement.parentNode;
-			}
-		}
-		const elements = rootElement.querySelectorAll(selector);
-
 		// eslint-disable-next-line
 		let parent: IElement = this;
+
 		while (parent) {
-			if (elements.includes(parent)) {
+			if (QuerySelector.match(parent, selector)) {
 				return parent;
 			}
 			parent = parent.parentElement;
 		}
 
-		// QuerySelectorAll() will not match the element it is looking in when searched for
-		// Therefore we need to check if it matches the root
-		if (rootElement.matches(selector)) {
-			return rootElement;
-		}
-
 		return null;
 	}
 
diff --git a/packages/happy-dom/src/nodes/node/Node.ts b/packages/happy-dom/src/nodes/node/Node.ts
index b4594f59..62e7f6ad 100644
--- a/packages/happy-dom/src/nodes/node/Node.ts
+++ b/packages/happy-dom/src/nodes/node/Node.ts
@@ -11,17 +11,6 @@ import IAttr from '../attr/IAttr';
 import NodeList from './NodeList';
 import INodeList from './INodeList';
 
-const JSON_CIRCULAR_PROPERTIES = [
-	'ownerDocument',
-	'parentNode',
-	'ownerElement',
-	'defaultView',
-	'_rootNode',
-	'_formNode',
-	'_selectNode',
-	'_textAreaNode'
-];
-
 /**
  * Node.
  */
@@ -198,7 +187,7 @@ export default class Node extends EventTarget implements INode {
 	 */
 	public get parentElement(): IElement {
 		let parent = this.parentNode;
-		while (parent && parent.nodeType !== Node.ELEMENT_NODE) {
+		while (parent && parent.nodeType !== NodeTypeEnum.elementNode) {
 			parent = parent.parentNode;
 		}
 		return <IElement>parent;
@@ -648,17 +637,4 @@ export default class Node extends EventTarget implements INode {
 			child = child.nextSibling;
 		}
 	}
-
-	/**
-	 * This will be called by JSON.stringify() when serializing this node.
-	 *
-	 * @returns Object without circular references.
-	 */
-	public toJSON(): { [key: string]: unknown } {
-		const result = {};
-		for (const key of Object.keys(this)) {
-			result[key] = !JSON_CIRCULAR_PROPERTIES.includes(key) ? this[key] : null;
-		}
-		return result;
-	}
 }
diff --git a/packages/happy-dom/src/query-selector/ISelectorAttribute.ts b/packages/happy-dom/src/query-selector/ISelectorAttribute.ts
new file mode 100644
index 00000000..403a5137
--- /dev/null
+++ b/packages/happy-dom/src/query-selector/ISelectorAttribute.ts
@@ -0,0 +1,5 @@
+export default interface ISelectorAttribute {
+	name: string;
+	operator: string | null;
+	value: string | null;
+}
diff --git a/packages/happy-dom/src/query-selector/ISelectorMatch.ts b/packages/happy-dom/src/query-selector/ISelectorMatch.ts
new file mode 100644
index 00000000..52c57e35
--- /dev/null
+++ b/packages/happy-dom/src/query-selector/ISelectorMatch.ts
@@ -0,0 +1,3 @@
+export default interface ISelectorMatch {
+	priorityWeight: number;
+}
diff --git a/packages/happy-dom/src/query-selector/QuerySelector.ts b/packages/happy-dom/src/query-selector/QuerySelector.ts
index 207ec41b..e6f796cf 100644
--- a/packages/happy-dom/src/query-selector/QuerySelector.ts
+++ b/packages/happy-dom/src/query-selector/QuerySelector.ts
@@ -1,12 +1,13 @@
-import Element from '../nodes/element/Element';
 import IElement from '../nodes/element/IElement';
-import INode from '../nodes/node/INode';
-import Node from '../nodes/node/Node';
 import INodeList from '../nodes/node/INodeList';
 import SelectorItem from './SelectorItem';
 import NodeList from '../nodes/node/NodeList';
-
-const SELECTOR_PART_REGEXP = /(\[[^\]]+\]|[a-zA-Z0-9-_.#"*:()\]]+)|([ ,>]+)/g;
+import NodeTypeEnum from '../nodes/node/NodeTypeEnum';
+import SelectorCombinatorEnum from './SelectorCombinatorEnum';
+import IDocument from '../nodes/document/IDocument';
+import IDocumentFragment from '../nodes/document-fragment/IDocumentFragment';
+import SelectorParser from './SelectorParser';
+import ISelectorMatch from './ISelectorMatch';
 
 /**
  * Utility for query selection in an HTML element.
@@ -21,26 +22,36 @@ export default class QuerySelector {
 	 * @param selector Selector.
 	 * @returns HTML elements.
 	 */
-	public static querySelectorAll(node: INode, selector: string): INodeList<IElement> {
-		const matches = new NodeList<IElement>();
+	public static querySelectorAll(
+		node: IElement | IDocument | IDocumentFragment,
+		selector: string
+	): INodeList<IElement> {
+		const nodeList = new NodeList<IElement>();
+		const allMatches = {};
 
 		if (selector === '') {
 			throw new Error(
 				"Failed to execute 'querySelectorAll' on 'Element': The provided selector is empty."
 			);
 		}
+
 		if (selector === null || selector === undefined) {
-			return matches;
+			return nodeList;
 		}
-		for (const parts of this.getSelectorParts(selector)) {
-			for (const element of this.findAll(node, [node], parts)) {
-				if (!matches.includes(element)) {
-					matches.push(element);
-				}
-			}
+
+		for (const items of SelectorParser.getSelectorGroups(selector)) {
+			const matches =
+				node.nodeType === NodeTypeEnum.elementNode
+					? this.findAll(<IElement>node, [<IElement>node], items)
+					: this.findAll(null, node.children, items);
+			Object.assign(allMatches, matches);
+		}
+
+		for (const key of Object.keys(allMatches).sort()) {
+			nodeList.push(allMatches[key]);
 		}
 
-		return matches;
+		return nodeList;
 	}
 
 	/**
@@ -50,17 +61,25 @@ export default class QuerySelector {
 	 * @param selector Selector.
 	 * @returns HTML element.
 	 */
-	public static querySelector(node: INode, selector: string): IElement {
+	public static querySelector(
+		node: IElement | IDocument | IDocumentFragment,
+		selector: string
+	): IElement {
 		if (selector === '') {
 			throw new Error(
 				"Failed to execute 'querySelector' on 'Element': The provided selector is empty."
 			);
 		}
+
 		if (selector === null || selector === undefined) {
 			return null;
 		}
-		for (const parts of this.getSelectorParts(selector)) {
-			const match = this.findFirst(node, [node], parts);
+
+		for (const items of SelectorParser.getSelectorGroups(selector)) {
+			const match =
+				node.nodeType === NodeTypeEnum.elementNode
+					? this.findFirst(<IElement>node, [<IElement>node], items)
+					: this.findFirst(null, node.children, items);
 
 			if (match) {
 				return match;
@@ -71,115 +90,153 @@ export default class QuerySelector {
 	}
 
 	/**
-	 * Checks if a node matches a selector and returns priority weight.
+	 * Checks if an element matches a selector and returns priority weight.
 	 *
-	 * @param node Node to search in.
-	 * @param selector Selector.
+	 * @param element Element to match.
+	 * @param selector Selector to match with.
 	 * @returns Result.
 	 */
-	public static match(node: INode, selector: string): { priorityWeight: number; matches: boolean } {
-		for (const parts of this.getSelectorParts(selector)) {
-			const result = this.matchesSelector(node, node, parts.reverse());
+	public static match(element: IElement, selector: string): ISelectorMatch | null {
+		for (const items of SelectorParser.getSelectorGroups(selector)) {
+			const result = this.matchSelector(element, element, items.reverse());
 
-			if (result.matches) {
+			if (result) {
 				return result;
 			}
 		}
 
-		return { priorityWeight: 0, matches: false };
+		return null;
 	}
 
 	/**
 	 * Checks if a node matches a selector.
 	 *
-	 * @param targetNode Target node.
-	 * @param currentNode Current node.
-	 * @param selectorParts Selector parts.
+	 * @param targetElement Target element.
+	 * @param currentElement Current element.
+	 * @param selectorItems Selector items.
 	 * @param [priorityWeight] Priority weight.
 	 * @returns Result.
 	 */
-	private static matchesSelector(
-		targetNode: INode,
-		currentNode: INode,
-		selectorParts: string[],
+	private static matchSelector(
+		targetElement: IElement,
+		currentElement: IElement,
+		selectorItems: SelectorItem[],
 		priorityWeight = 0
-	): {
-		priorityWeight: number;
-		matches: boolean;
-	} {
-		const isDirectChild = selectorParts[0] === '>';
-		if (isDirectChild) {
-			selectorParts = selectorParts.slice(1);
-			if (selectorParts.length === 0) {
-				return { priorityWeight: 0, matches: false };
+	): ISelectorMatch | null {
+		const selectorItem = selectorItems[0];
+		const result = selectorItem.match(currentElement);
+
+		if (result) {
+			if (selectorItems.length === 1) {
+				return {
+					priorityWeight: priorityWeight + result.priorityWeight
+				};
 			}
-		}
 
-		if (selectorParts.length === 0) {
-			return { priorityWeight, matches: true };
-		}
-
-		const selector = new SelectorItem(selectorParts[0]);
-		const result = selector.match(<IElement>currentNode);
+			switch (selectorItem.combinator) {
+				case SelectorCombinatorEnum.adjacentSibling:
+					if (currentElement.previousElementSibling) {
+						const match = this.matchSelector(
+							targetElement,
+							currentElement.previousElementSibling,
+							selectorItems.slice(1),
+							priorityWeight + result.priorityWeight
+						);
 
-		if (result.matches && selectorParts.length === 1) {
-			return {
-				priorityWeight: priorityWeight + result.priorityWeight,
-				matches: true
-			};
+						if (match) {
+							return match;
+						}
+					}
+					break;
+				case SelectorCombinatorEnum.child:
+				case SelectorCombinatorEnum.descendant:
+					if (currentElement.parentElement) {
+						const match = this.matchSelector(
+							targetElement,
+							currentElement.parentElement,
+							selectorItems.slice(1),
+							priorityWeight + result.priorityWeight
+						);
+						if (match) {
+							return match;
+						}
+					}
+					break;
+			}
 		}
 
-		if (!currentNode.parentElement || (targetNode === currentNode && !result.matches)) {
-			return { priorityWeight: 0, matches: false };
+		if (
+			selectorItem.combinator === SelectorCombinatorEnum.descendant &&
+			targetElement !== currentElement &&
+			currentElement.parentElement
+		) {
+			return this.matchSelector(
+				targetElement,
+				currentElement.parentElement,
+				selectorItems,
+				priorityWeight
+			);
 		}
 
-		return this.matchesSelector(
-			isDirectChild ? currentNode.parentElement : targetNode,
-			currentNode.parentElement,
-			result.matches ? selectorParts.slice(1) : selectorParts,
-			priorityWeight + result.priorityWeight
-		);
+		return null;
 	}
 
 	/**
 	 * Finds elements based on a query selector for a part of a list of selectors separated with comma.
 	 *
-	 * @param rootNode Root node.
-	 * @param nodes Nodes.
-	 * @param selectorParts Selector parts.
-	 * @param [selectorItem] Selector item.
-	 * @returns HTML elements.
+	 * @param rootElement Root element.
+	 * @param children Child elements.
+	 * @param selectorItems Selector items.
+	 * @param [documentPosition] Document position of the element.
+	 * @returns Document position and element map.
 	 */
 	private static findAll(
-		rootNode: INode,
-		nodes: INode[],
-		selectorParts: string[],
-		selectorItem?: SelectorItem
-	): IElement[] {
-		const isDirectChild = selectorParts[0] === '>';
-		if (isDirectChild) {
-			selectorParts = selectorParts.slice(1);
-		}
-		const selector = selectorItem || new SelectorItem(selectorParts[0]);
-		let matched = [];
-
-		for (const node of nodes) {
-			if (node.nodeType === Node.ELEMENT_NODE) {
-				if (selector.match(<Element>node).matches) {
-					if (selectorParts.length === 1) {
-						if (rootNode !== node) {
-							matched.push(node);
-						}
-					} else {
-						matched = matched.concat(
-							this.findAll(rootNode, (<Element>node).children, selectorParts.slice(1), null)
-						);
+		rootElement: IElement,
+		children: IElement[],
+		selectorItems: SelectorItem[],
+		documentPosition?: string
+	): { [documentPosition: string]: IElement } {
+		const selectorItem = selectorItems[0];
+		const nextSelectorItem = selectorItems[1];
+		const matched: { [documentPosition: string]: IElement } = {};
+
+		for (let i = 0, max = children.length; i < max; i++) {
+			const child = children[i];
+			const position = (documentPosition ? documentPosition + '>' : '') + i;
+
+			if (selectorItem.match(child)) {
+				if (!nextSelectorItem) {
+					if (rootElement !== child) {
+						matched[position] = child;
+					}
+				} else {
+					switch (nextSelectorItem.combinator) {
+						case SelectorCombinatorEnum.adjacentSibling:
+							if (child.nextElementSibling) {
+								Object.assign(
+									matched,
+									this.findAll(
+										rootElement,
+										[child.nextElementSibling],
+										selectorItems.slice(1),
+										position
+									)
+								);
+							}
+							break;
+						case SelectorCombinatorEnum.descendant:
+						case SelectorCombinatorEnum.child:
+							Object.assign(
+								matched,
+								this.findAll(rootElement, child.children, selectorItems.slice(1), position)
+							);
+							break;
 					}
 				}
 			}
 
-			if (!isDirectChild && node['children']) {
-				matched = matched.concat(this.findAll(rootNode, node['children'], selectorParts, selector));
+			if (selectorItem.combinator === SelectorCombinatorEnum.descendant && child.children.length) {
+				Object.assign(matched, this.findAll(rootElement, child.children, selectorItems, position));
 			}
 		}
 
@@ -189,99 +246,59 @@ export default class QuerySelector {
 	/**
 	 * Finds an element based on a query selector for a part of a list of selectors separated with comma.
 	 *
-	 * @param rootNode
-	 * @param nodes Nodes.
-	 * @param selector Selector.
-	 * @param selectorParts
-	 * @param [selectorItem] Selector item.
+	 * @param rootElement Root element.
+	 * @param children Child elements.
+	 * @param selectorItems Selector items.
 	 * @returns HTML element.
 	 */
 	private static findFirst(
-		rootNode: INode,
-		nodes: INode[],
-		selectorParts: string[],
-		selectorItem?: SelectorItem
+		rootElement: IElement,
+		children: IElement[],
+		selectorItems: SelectorItem[]
 	): IElement {
-		const isDirectChild = selectorParts[0] === '>';
-		if (isDirectChild) {
-			selectorParts = selectorParts.slice(1);
-		}
-		const selector = selectorItem || new SelectorItem(selectorParts[0]);
-
-		for (const node of nodes) {
-			if (node.nodeType === Node.ELEMENT_NODE && selector.match(<Element>node).matches) {
-				if (selectorParts.length === 1) {
-					if (rootNode !== node) {
-						return <Element>node;
+		const selectorItem = selectorItems[0];
+		const nextSelectorItem = selectorItems[1];
+
+		for (const child of children) {
+			if (selectorItem.match(child)) {
+				if (!nextSelectorItem) {
+					if (rootElement !== child) {
+						return child;
 					}
 				} else {
-					const childSelector = this.findFirst(
-						rootNode,
-						(<Element>node).children,
-						selectorParts.slice(1),
-						null
-					);
-					if (childSelector) {
-						return childSelector;
+					switch (nextSelectorItem.combinator) {
+						case SelectorCombinatorEnum.adjacentSibling:
+							if (child.nextElementSibling) {
+								const match = this.findFirst(
+									rootElement,
+									[child.nextElementSibling],
+									selectorItems.slice(1)
+								);
+								if (match) {
+									return match;
+								}
+							}
+							break;
+						case SelectorCombinatorEnum.descendant:
+						case SelectorCombinatorEnum.child:
+							const match = this.findFirst(rootElement, child.children, selectorItems.slice(1));
+							if (match) {
+								return match;
+							}
+							break;
 					}
 				}
 			}
 
-			if (!isDirectChild && node['children']) {
-				const childSelector = this.findFirst(rootNode, node['children'], selectorParts, selector);
+			if (selectorItem.combinator === SelectorCombinatorEnum.descendant && child.children.length) {
+				const match = this.findFirst(rootElement, child.children, selectorItems);
 
-				if (childSelector) {
-					return childSelector;
+				if (match) {
+					return match;
 				}
 			}
 		}
 
 		return null;
 	}
-
-	/**
-	 * Splits a selector string into groups and parts.
-	 *
-	 * @param selector Selector.
-	 * @returns HTML element.
-	 */
-	private static getSelectorParts(selector: string): string[][] {
-		if (selector === '*' || (!selector.includes(',') && !selector.includes(' '))) {
-			return [[selector]];
-		}
-
-		const regexp = new RegExp(SELECTOR_PART_REGEXP);
-		const groups = [];
-		let currentSelector = '';
-		let parts = [];
-		let match;
-
-		while ((match = regexp.exec(selector))) {
-			if (match[2]) {
-				const trimmed = match[2].trim();
-
-				parts.push(currentSelector);
-				currentSelector = '';
-
-				if (trimmed === ',') {
-					groups.push(parts);
-					parts = [];
-				} else if (trimmed === '>') {
-					parts.push('>');
-				}
-			} else if (match[1]) {
-				currentSelector += match[1];
-			}
-		}
-
-		if (currentSelector !== '') {
-			parts.push(currentSelector);
-		}
-
-		if (parts.length > 0) {
-			groups.push(parts);
-		}
-
-		return groups;
-	}
 }
diff --git a/packages/happy-dom/src/query-selector/SelectorCombinatorEnum.ts b/packages/happy-dom/src/query-selector/SelectorCombinatorEnum.ts
new file mode 100644
index 00000000..07a18a8d
--- /dev/null
+++ b/packages/happy-dom/src/query-selector/SelectorCombinatorEnum.ts
@@ -0,0 +1,7 @@
+enum SelectorCombinatorEnum {
+	descendant = 'descendant',
+	child = 'child',
+	adjacentSibling = 'adjacentSibling'
+}
+
+export default SelectorCombinatorEnum;
diff --git a/packages/happy-dom/src/query-selector/SelectorItem.ts b/packages/happy-dom/src/query-selector/SelectorItem.ts
index 8058909b..137991de 100644
--- a/packages/happy-dom/src/query-selector/SelectorItem.ts
+++ b/packages/happy-dom/src/query-selector/SelectorItem.ts
@@ -2,53 +2,55 @@ import DOMException from '../exception/DOMException';
 import IElement from '../nodes/element/IElement';
 import Element from '../nodes/element/Element';
 import IHTMLInputElement from '../nodes/html-input-element/IHTMLInputElement';
-
-const ATTRIBUTE_REGEXP =
-	/\[([a-zA-Z0-9-_]+)\]|\[([a-zA-Z0-9-_]+)([~|^$*]{0,1})[ ]*=[ ]*["']{0,1}([^"']+)["']{0,1}\]/g;
-const ATTRIBUTE_NAME_REGEXP = /[^a-zA-Z0-9-_$]/;
-const PSUEDO_REGEXP =
-	/(?<!\\):([a-zA-Z-]+)\(([0-9n+-]+|odd|even)\)|(?<!\\):not\(([^)]+)\)|(?<!\\):([a-zA-Z-]+)/g;
-const CLASS_REGEXP = /\.(([a-zA-Z0-9-_$]|\\.)+)/g;
-const TAG_NAME_REGEXP = /^[a-zA-Z0-9-]+/;
-const ID_REGEXP = /(?<!\\)#[A-Za-z][-A-Za-z0-9_]*/g;
-const CSS_ESCAPE_REGEXP = /(?<!\\):/g;
-const CSS_ESCAPE_CHAR_REGEXP = /\\/g;
+import SelectorCombinatorEnum from './SelectorCombinatorEnum';
+import ISelectorAttribute from './ISelectorAttribute';
+import SelectorParser from './SelectorParser';
+import ISelectorMatch from './ISelectorMatch';
 
 /**
  * Selector item.
  */
 export default class SelectorItem {
-	private isAll: boolean;
-	private isID: boolean;
-	private isAttribute: boolean;
-	private isPseudo: boolean;
-	private isClass: boolean;
-	private isTagName: boolean;
-	private tagName = null;
-	private selector: string;
-	private id: string;
+	public all: string | null;
+	public tagName: string | null;
+	public id: string | null;
+	public classNames: string[] | null;
+	public attributes: ISelectorAttribute[] | null;
+	public pseudoClass: string | null;
+	public pseudoArguments: string | null;
+	public combinator: SelectorCombinatorEnum;
 
 	/**
 	 * Constructor.
 	 *
-	 * @param selector Selector.
+	 * @param [options] Options.
+	 * @param [options.combinator] Combinator.
+	 * @param [options.all] All.
+	 * @param [options.tagName] Tag name.
+	 * @param [options.id] ID.
+	 * @param [options.classNames] Class names.
+	 * @param [options.attributes] Attributes.
+	 * @param [options.pseudoClass] Pseudo class.
+	 * @param [options.pseudoArguments] Pseudo arguments.
 	 */
-	constructor(selector: string) {
-		const baseSelector = selector.replace(new RegExp(PSUEDO_REGEXP, 'g'), '');
-		const idMatch = !this.isAll && selector.includes('#') ? baseSelector.match(ID_REGEXP) : null;
-
-		this.isAll = baseSelector === '*';
-		this.isID = !!idMatch;
-		this.isAttribute = !this.isAll && baseSelector.includes('[');
-		// If baseSelector !== selector then some psuedo selector was replaced above
-		this.isPseudo = !this.isAll && baseSelector !== selector;
-		this.isClass =
-			!this.isAll && new RegExp(CLASS_REGEXP, 'g').test(baseSelector) && !this.isAttribute;
-		this.tagName = !this.isAll ? baseSelector.match(TAG_NAME_REGEXP) : null;
-		this.tagName = this.tagName ? this.tagName[0].toUpperCase() : null;
-		this.isTagName = this.tagName !== null;
-		this.selector = selector;
-		this.id = idMatch ? idMatch[0].replace('#', '') : null;
+	constructor(options?: {
+		all?: string;
+		tagName?: string;
+		id?: string;
+		classNames?: string[];
+		attributes?: ISelectorAttribute[];
+		pseudoClass?: string;
+		pseudoArguments?: string;
+		combinator?: SelectorCombinatorEnum;
+	}) {
+		this.all = options?.all || null;
+		this.tagName = options?.tagName || null;
+		this.id = options?.id || null;
+		this.classNames = options?.classNames || null;
+		this.attributes = options?.attributes || null;
+		this.pseudoClass = options?.pseudoClass || null;
+		this.pseudoArguments = options?.pseudoArguments || null;
+		this.combinator = options?.combinator || SelectorCombinatorEnum.descendant;
 	}
 
 	/**
@@ -57,176 +59,97 @@ export default class SelectorItem {
 	 * @param element HTML element.
 	 * @returns Result.
 	 */
-	public match(element: IElement): { priorityWeight: number; matches: boolean } {
-		const selector = this.selector;
-
+	public match(element: IElement): ISelectorMatch | null {
 		let priorityWeight = 0;
 
-		// Is all (*)
-		if (this.isAll) {
-			return { priorityWeight: 0, matches: true };
+		// Tag name match
+		if (this.tagName) {
+			if (this.tagName !== element.tagName) {
+				return null;
+			}
+			priorityWeight += 1;
 		}
 
 		// ID Match
-		if (this.isID) {
-			priorityWeight += 100;
-
+		if (this.id) {
 			if (this.id !== element.id) {
-				return { priorityWeight: 0, matches: false };
-			}
-		}
-
-		// Tag name match
-		if (this.isTagName) {
-			priorityWeight += 1;
-
-			if (this.tagName !== element.tagName) {
-				return { priorityWeight: 0, matches: false };
+				return null;
 			}
+			priorityWeight += 100;
 		}
 
 		// Class match
-		if (this.isClass) {
-			const result = this.matchesClass(element, selector);
-			priorityWeight += result.priorityWeight;
-			if (!result.matches) {
-				return { priorityWeight: 0, matches: false };
+		if (this.classNames) {
+			const result = this.matchClass(element);
+			if (!result) {
+				return null;
 			}
-		}
-
-		// Pseudo match
-		if (this.isPseudo && !this.matchesPsuedo(element, selector)) {
-			return { priorityWeight: 0, matches: false };
+			priorityWeight += result.priorityWeight;
 		}
 
 		// Attribute match
-		if (this.isAttribute) {
-			const result = this.matchesAttribute(element, selector);
-			priorityWeight += result.priorityWeight;
-			if (!result.matches) {
-				return { priorityWeight: 0, matches: false };
+		if (this.attributes) {
+			const result = this.matchAttributes(element);
+			if (!result) {
+				return null;
 			}
+			priorityWeight += result.priorityWeight;
 		}
 
-		return { priorityWeight, matches: true };
-	}
-
-	/**
-	 * Matches a psuedo selector.
-	 *
-	 * @param element Element.
-	 * @param selector Selector.
-	 * @returns True if it is a match.
-	 */
-	private matchesPsuedo(element: IElement, selector: string): boolean {
-		const regexp = new RegExp(PSUEDO_REGEXP, 'g');
-		let match: RegExpMatchArray;
-
-		while ((match = regexp.exec(selector))) {
-			const isNotClass = match[3] && match[3].trim()[0] === '.';
-			if (match[1] && !this.matchesNthChild(element, match[1], match[2])) {
-				return false;
-			} else if (match[3]) {
-				if (isNotClass && this.matchesClass(element, match[3]).matches) {
-					return false;
-				}
-				if (
-					!isNotClass &&
-					match[3].includes('[') &&
-					this.matchesAttribute(element, match[3]).matches
-				) {
-					return false;
-				}
-				if (!isNotClass && element.tagName.toLowerCase() === match[3]) {
-					return false;
-				}
-			} else if (match[4] && !this.matchesPsuedoExpression(element, match[4])) {
-				return false;
-			}
+		// Pseudo match
+		if (this.pseudoClass && !this.matchPsuedo(element)) {
+			return null;
 		}
 
-		return true;
+		return { priorityWeight };
 	}
 
 	/**
-	 * Matches a nth-child selector.
+	 * Matches a psuedo selector.
 	 *
 	 * @param element Element.
-	 * @param psuedo Psuedo name.
-	 * @param place Place.
-	 * @returns True if it is a match.
+	 * @returns Result.
 	 */
-	private matchesNthChild(element: IElement, psuedo: string, place: string): boolean {
-		let children = element.parentNode ? (<IElement>element.parentNode).children : [];
+	private matchPsuedo(element: IElement): boolean {
+		const parent = <IElement>element.parentNode;
 
-		switch (psuedo.toLowerCase()) {
+		// Validation
+		switch (this.pseudoClass) {
+			case 'not':
+			case 'nth-child':
 			case 'nth-of-type':
-				children = children.filter((child) => child.tagName === element.tagName);
-				break;
 			case 'nth-last-child':
-				children = children.reverse();
-				break;
 			case 'nth-last-of-type':
-				children = children.filter((child) => child.tagName === element.tagName).reverse();
-				break;
-		}
-
-		if (place === 'odd') {
-			const index = children.indexOf(element);
-			return index !== -1 && (index + 1) % 2 !== 0;
-		} else if (place === 'even') {
-			const index = children.indexOf(element);
-			return index !== -1 && (index + 1) % 2 === 0;
-		} else if (place.includes('n')) {
-			const [a, b] = place.replace(/ /g, '').split('n');
-			const childIndex = children.indexOf(element);
-			const aNumber = a !== '' ? Number(a) : 1;
-			const bNumber = b !== undefined ? Number(b) : 0;
-			if (isNaN(aNumber) || isNaN(bNumber)) {
-				throw new DOMException(`The selector "${this.selector}" is not valid.`);
-			}
-
-			for (let i = 0, max = children.length; i <= max; i += aNumber) {
-				if (childIndex === i + bNumber - 1) {
-					return true;
+				if (!this.pseudoArguments) {
+					throw new DOMException(`The selector "${this.getSelectorString()}" is not valid.`);
 				}
-			}
-
-			return false;
-		}
-
-		const number = Number(place);
-
-		if (isNaN(number)) {
-			throw new DOMException(`The selector "${this.selector}" is not valid.`);
+				break;
 		}
 
-		return children[number - 1] === element;
-	}
-
-	/**
-	 * Matches a psuedo selector expression.
-	 *
-	 * @param element Element.
-	 * @param psuedo Psuedo name.
-	 * @returns True if it is a match.
-	 */
-	private matchesPsuedoExpression(element: IElement, psuedo: string): boolean {
-		const parent = <IElement>element.parentNode;
-
+		// Check if parent exists
 		if (!parent) {
-			return false;
+			switch (this.pseudoClass) {
+				case 'first-child':
+				case 'last-child':
+				case 'only-child':
+				case 'first-of-type':
+				case 'last-of-type':
+				case 'only-of-type':
+				case 'nth-child':
+				case 'nth-of-type':
+				case 'nth-last-child':
+				case 'nth-last-of-type':
+					return false;
+			}
 		}
 
-		switch (psuedo.toLowerCase()) {
+		switch (this.pseudoClass) {
 			case 'first-child':
 				return parent.children[0] === element;
 			case 'last-child':
-				const lastChildChildren = parent.children;
-				return lastChildChildren[lastChildChildren.length - 1] === element;
+				return parent.children.length && parent.children[parent.children.length - 1] === element;
 			case 'only-child':
-				const onlyChildChildren = parent.children;
-				return onlyChildChildren.length === 1 && onlyChildChildren[0] === element;
+				return parent.children.length === 1 && parent.children[0] === element;
 			case 'first-of-type':
 				for (const child of parent.children) {
 					if (child.tagName === element.tagName) {
@@ -255,139 +178,190 @@ export default class SelectorItem {
 				return isFound;
 			case 'checked':
 				return element.tagName === 'INPUT' && (<IHTMLInputElement>element).checked;
+			case 'empty':
+				return !element.children.length;
+			case 'root':
+				return element.tagName === 'HTML';
+			case 'not':
+				return !SelectorParser.getSelectorItem(this.pseudoArguments).match(element);
+			case 'nth-child':
+				return this.matchNthChild(element, parent.children, this.pseudoArguments);
+			case 'nth-of-type':
+				if (!element.parentNode) {
+					return false;
+				}
+				return this.matchNthChild(
+					element,
+					parent.children.filter((child) => child.tagName === element.tagName),
+					this.pseudoArguments
+				);
+			case 'nth-last-child':
+				return this.matchNthChild(element, parent.children.reverse(), this.pseudoArguments);
+			case 'nth-last-of-type':
+				return this.matchNthChild(
+					element,
+					parent.children.filter((child) => child.tagName === element.tagName).reverse(),
+					this.pseudoArguments
+				);
 		}
-
-		return false;
 	}
 
 	/**
-	 * Matches attribute.
+	 * Matches a nth-child selector.
 	 *
 	 * @param element Element.
-	 * @param selector Selector.
-	 * @returns Result.
+	 * @param parentChildren Parent children.
+	 * @param placement Placement.
+	 * @returns True if it is a match.
 	 */
-	private matchesAttribute(
-		element: IElement,
-		selector: string
-	): { priorityWeight: number; matches: boolean } {
-		const regexp = new RegExp(ATTRIBUTE_REGEXP, 'g');
-		let match: RegExpMatchArray;
-		let priorityWeight = 0;
+	private matchNthChild(element: IElement, parentChildren: IElement[], placement: string): boolean {
+		if (placement === 'odd') {
+			const index = parentChildren.indexOf(element);
+			return index !== -1 && (index + 1) % 2 !== 0;
+		} else if (placement === 'even') {
+			const index = parentChildren.indexOf(element);
+			return index !== -1 && (index + 1) % 2 === 0;
+		} else if (placement.includes('n')) {
+			const [a, b] = placement.replace(/ /g, '').split('n');
+			const childIndex = parentChildren.indexOf(element);
+			const aNumber = a !== '' ? Number(a) : 1;
+			const bNumber = b !== undefined ? Number(b) : 0;
+			if (isNaN(aNumber) || isNaN(bNumber)) {
+				throw new DOMException(`The selector "${this.getSelectorString()}" is not valid.`);
+			}
 
-		while ((match = regexp.exec(selector))) {
-			const isPsuedo = match.index > 0 && selector[match.index - 1] === '(';
+			for (let i = 0, max = parentChildren.length; i <= max; i += aNumber) {
+				if (childIndex === i + bNumber - 1) {
+					return true;
+				}
+			}
 
-			priorityWeight += 10;
+			return false;
+		}
 
-			if (
-				!isPsuedo &&
-				((match[1] && !this.matchesAttributeName(element, match[1])) ||
-					(match[2] && !this.matchesAttributeNameAndValue(element, match[2], match[4], match[3])))
-			) {
-				return { priorityWeight: 0, matches: false };
-			}
+		const number = Number(placement);
+
+		if (isNaN(number)) {
+			throw new DOMException(`The selector "${this.getSelectorString()}" is not valid.`);
 		}
 
-		return { priorityWeight, matches: true };
+		return parentChildren[number - 1] === element;
 	}
 
 	/**
-	 * Matches class.
+	 * Matches attribute.
 	 *
 	 * @param element Element.
-	 * @param selector Selector.
 	 * @returns Result.
 	 */
-	private matchesClass(
-		element: IElement,
-		selector: string
-	): { priorityWeight: number; matches: boolean } {
-		const regexp = new RegExp(CLASS_REGEXP, 'g');
-		const classList = element.className.split(' ');
-		const classSelector = selector.split(CSS_ESCAPE_REGEXP)[0];
+	private matchAttributes(element: IElement): ISelectorMatch | null {
+		if (!this.attributes) {
+			return null;
+		}
+
 		let priorityWeight = 0;
-		let match: RegExpMatchArray;
 
-		while ((match = regexp.exec(classSelector))) {
+		for (const attribute of this.attributes) {
+			const elementAttribute = (<Element>element)._attributes[attribute.name];
+
+			if (!elementAttribute) {
+				return null;
+			}
+
 			priorityWeight += 10;
-			if (!classList.includes(match[1].replace(CSS_ESCAPE_CHAR_REGEXP, ''))) {
-				return { priorityWeight: 0, matches: false };
+
+			if (attribute.value !== null) {
+				if (!elementAttribute.value) {
+					return null;
+				}
+
+				switch (attribute.operator) {
+					// [attribute~="value"] - Contains a specified word.
+					case null:
+						if (attribute.value !== elementAttribute.value) {
+							return null;
+						}
+						break;
+					// [attribute~="value"] - Contains a specified word.
+					case '~':
+						if (!elementAttribute.value.split(' ').includes(attribute.value)) {
+							return null;
+						}
+						break;
+					// [attribute|="value"] - Starts with the specified word.
+					case '|':
+						if (!new RegExp(`^${attribute.value}[- ]`).test(elementAttribute.value)) {
+							return null;
+						}
+						break;
+					// [attribute^="value"] - Begins with a specified value.
+					case '^':
+						if (!elementAttribute.value.startsWith(attribute.value)) {
+							return null;
+						}
+						break;
+					// [attribute$="value"] - Ends with a specified value.
+					case '$':
+						if (!elementAttribute.value.endsWith(attribute.value)) {
+							return null;
+						}
+						break;
+					// [attribute*="value"] - Contains a specified value.
+					case '*':
+						if (!elementAttribute.value.includes(attribute.value)) {
+							return null;
+						}
+						break;
+				}
 			}
 		}
 
-		return { priorityWeight, matches: true };
+		return { priorityWeight };
 	}
 
 	/**
-	 * Matches attribute name only.
+	 * Matches class.
 	 *
 	 * @param element Element.
-	 * @param attributeName Attribute name.
-	 * @returns True if it is a match.
+	 * @returns Result.
 	 */
-	private matchesAttributeName(element: IElement, attributeName: string): boolean {
-		if (ATTRIBUTE_NAME_REGEXP.test(attributeName)) {
-			throw new DOMException(`The selector "${this.selector}" is not valid.`);
+	private matchClass(element: IElement): ISelectorMatch | null {
+		if (!this.classNames) {
+			return null;
 		}
 
-		return !!(<Element>element)._attributes[attributeName.toLowerCase()];
+		const classList = element.className.split(' ');
+		let priorityWeight = 0;
+
+		for (const className of this.classNames) {
+			if (!classList.includes(className)) {
+				return null;
+			}
+			priorityWeight += 10;
+		}
+
+		return { priorityWeight };
 	}
 
-	/** .
-	 *
-	 * Matches attribute name and value.
-	 *
-	 * @param element Element.
-	 * @param attributeName Attribute name.
-	 * @param attributeValue Attribute value.
-	 * @param [matchType] Match type.
-	 * @returns True if it is a match.
-	 */
 	/**
+	 * Returns the selector string.
 	 *
-	 * @param element
-	 * @param attributeName
-	 * @param attributeValue
-	 * @param matchType
+	 * @returns Selector string.
 	 */
-	private matchesAttributeNameAndValue(
-		element: IElement,
-		attributeName: string,
-		attributeValue: string,
-		matchType: string = null
-	): boolean {
-		const attribute = (<Element>element)._attributes[attributeName.toLowerCase()];
-		const value = attributeValue;
-
-		if (ATTRIBUTE_NAME_REGEXP.test(attributeName)) {
-			throw new DOMException(`The selector "${this.selector}" is not valid.`);
-		}
-
-		if (!attribute) {
-			return false;
-		}
-
-		if (matchType) {
-			switch (matchType) {
-				// [attribute~="value"] - Contains a specified word.
-				case '~':
-					return attribute.value && attribute.value.split(' ').includes(value);
-				// [attribute|="value"] - Starts with the specified word.
-				case '|':
-					return attribute && attribute.value && new RegExp(`^${value}[- ]`).test(attribute.value);
-				// [attribute^="value"] - Begins with a specified value.
-				case '^':
-					return attribute && attribute.value && attribute.value.startsWith(value);
-				// [attribute$="value"] - Ends with a specified value.
-				case '$':
-					return attribute && attribute.value && attribute.value.endsWith(value);
-				// [attribute*="value"] - Contains a specified value.
-				case '*':
-					return attribute && attribute.value && attribute.value.includes(value);
-			}
-		}
-
-		return attribute && attribute.value === value;
+	private getSelectorString(): string {
+		return `${this.all || ''}${this.tagName || ''}${this.id ? `#${this.id}` : ''}${
+			this.classNames ? `.${this.classNames.join('.')}` : ''
+		}${
+			this.attributes
+				? this.attributes
+						.map(
+							(attribute) =>
+								`[${attribute.name}${
+									attribute.value ? `${attribute.operator || ''}="${attribute.value}"` : ''
+								}]`
+						)
+						.join('')
+				: ''
+		}${this.pseudoClass || ''}${this.pseudoArguments ? `(${this.pseudoArguments})` : ''}`;
 	}
 }
diff --git a/packages/happy-dom/src/query-selector/SelectorParser.ts b/packages/happy-dom/src/query-selector/SelectorParser.ts
new file mode 100644
index 00000000..5923e319
--- /dev/null
+++ b/packages/happy-dom/src/query-selector/SelectorParser.ts
@@ -0,0 +1,148 @@
+import SelectorItem from './SelectorItem';
+import SelectorCombinatorEnum from './SelectorCombinatorEnum';
+import DOMException from '../exception/DOMException';
+
+/**
+ * Selector RegExp.
+ *
+ * Group 1: All (e.g. "*")
+ * Group 2: Tag name (e.g. "div")
+ * Group 3: ID (e.g. "#id")
+ * Group 4: Class (e.g. ".class")
+ * Group 5: Attribute name when no value (e.g. "attr1")
+ * Group 6: Attribute name when there is a value (e.g. "attr1")
+ * Group 7: Attribute operator (e.g. "~")
+ * Group 8: Attribute value (e.g. "value1")
+ * Group 9: Pseudo (e.g. "nth-child")
+ * Group 10: Arguments of pseudo (e.g. "2n + 1")
+ * Group 11: Combinator.
+ */
+const SELECTOR_REGEXP =
+	/(\*)|([a-zA-Z0-9-]+)|#([a-zA-Z0-9-_]+)|\.((?:[a-zA-Z0-9-_]|\\.)+)|\[([a-zA-Z0-9-_]+)\]|\[([a-zA-Z0-9-_]+)([~|^$*]{0,1}) *= *["']{0,1}([^"']+)["']{0,1}\]|:([a-zA-Z-:]+)|\(([^)]+)\)|([ ,+>]*)/g;
+
+/**
+ * Escaped Character RegExp.
+ */
+const CLASS_ESCAPED_CHARACTER_REGEXP = /\\/g;
+
+/**
+ * Simple Selector RegExp.
+ *
+ * Group 1: Tag name (e.g. "div")
+ * Group 2: Class (e.g. ".classA.classB")
+ * Group 3: ID (e.g. "#id")
+ */
+const SIMPLE_SELECTOR_REGEXP = /(^[a-zA-Z0-9-]+$)|(^\.[a-zA-Z0-9-_.]+$)|(^#[a-zA-Z0-9-_]+$)/;
+
+/**
+ * Utility for parsing a selection string.
+ */
+export default class SelectorParser {
+	/**
+	 * Parses a selector string and returns an instance of SelectorItem.
+	 *
+	 * @param selector Selector.
+	 * @returns Selector itme.
+	 */
+	public static getSelectorItem(selector: string): SelectorItem {
+		return this.getSelectorGroups(selector)[0][0];
+	}
+
+	/**
+	 * Parses a selector string and returns groups with SelectorItem instances.
+	 *
+	 * @param selector Selector.
+	 * @returns Selector groups.
+	 */
+	public static getSelectorGroups(selector: string): Array<Array<SelectorItem>> {
+		const simpleMatch = selector.match(SIMPLE_SELECTOR_REGEXP);
+
+		if (simpleMatch) {
+			if (simpleMatch[1]) {
+				return [[new SelectorItem({ tagName: selector.toUpperCase() })]];
+			} else if (simpleMatch[2]) {
+				return [[new SelectorItem({ classNames: selector.replace('.', '').split('.') })]];
+			} else if (simpleMatch[3]) {
+				return [[new SelectorItem({ id: selector.replace('#', '') })]];
+			}
+		}
+
+		const regexp = new RegExp(SELECTOR_REGEXP);
+		let currentSelectorItem: SelectorItem = new SelectorItem({
+			combinator: SelectorCombinatorEnum.descendant
+		});
+		let currentGroup: SelectorItem[] = [currentSelectorItem];
+		const groups: Array<Array<SelectorItem>> = [currentGroup];
+		let isValid = false;
+		let match;
+
+		while ((match = regexp.exec(selector))) {
+			if (match[0]) {
+				isValid = true;
+
+				if (match[1]) {
+					currentSelectorItem.all = '*';
+				} else if (match[2]) {
+					currentSelectorItem.tagName = match[2].toUpperCase();
+				} else if (match[3]) {
+					currentSelectorItem.id = match[3];
+				} else if (match[4]) {
+					currentSelectorItem.classNames = currentSelectorItem.classNames || [];
+					currentSelectorItem.classNames.push(match[4].replace(CLASS_ESCAPED_CHARACTER_REGEXP, ''));
+				} else if (match[5]) {
+					currentSelectorItem.attributes = currentSelectorItem.attributes || [];
+					currentSelectorItem.attributes.push({
+						name: match[5].toLowerCase(),
+						operator: null,
+						value: null
+					});
+				} else if (match[6] && match[8]) {
+					currentSelectorItem.attributes = currentSelectorItem.attributes || [];
+					currentSelectorItem.attributes.push({
+						name: match[6].toLowerCase(),
+						operator: match[7] || null,
+						value: match[8]
+					});
+				} else if (match[9]) {
+					currentSelectorItem.pseudoClass = match[9].toLowerCase();
+				} else if (match[10]) {
+					currentSelectorItem.pseudoArguments = match[10];
+				} else if (match[11]) {
+					switch (match[11].trim()) {
+						case ',':
+							currentSelectorItem = new SelectorItem({
+								combinator: SelectorCombinatorEnum.descendant
+							});
+							currentGroup = [currentSelectorItem];
+							groups.push(currentGroup);
+							break;
+						case '>':
+							currentSelectorItem = new SelectorItem({ combinator: SelectorCombinatorEnum.child });
+							currentGroup.push(currentSelectorItem);
+							break;
+						case '+':
+							currentSelectorItem = new SelectorItem({
+								combinator: SelectorCombinatorEnum.adjacentSibling
+							});
+							currentGroup.push(currentSelectorItem);
+							break;
+						case '':
+							currentSelectorItem = new SelectorItem({
+								combinator: SelectorCombinatorEnum.descendant
+							});
+							currentGroup.push(currentSelectorItem);
+							break;
+					}
+				}
+			} else {
+				break;
+			}
+		}
+
+		if (!isValid) {
+			throw new DOMException(`Invalid selector: "${selector}"`);
+		}
+
+		return groups;
+	}
+}


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 6d1049b9..eaaa3a1d 100644
--- a/packages/happy-dom/test/nodes/document/Document.test.ts
+++ b/packages/happy-dom/test/nodes/document/Document.test.ts
@@ -308,11 +308,11 @@ describe('Document', () => {
 				const styleSheets = document.styleSheets;
 
 				expect(styleSheets.length).toBe(2);
-				expect(styleSheets[0].cssRules.length).toBe(1);
-				expect(styleSheets[0].cssRules[0].cssText).toBe('button { background-color: red; }');
-				expect(styleSheets[1].cssRules.length).toBe(2);
-				expect(styleSheets[1].cssRules[0].cssText).toBe('body { background-color: red; }');
-				expect(styleSheets[1].cssRules[1].cssText).toBe('div { background-color: green; }');
+				expect(styleSheets[0].cssRules.length).toBe(2);
+				expect(styleSheets[0].cssRules[0].cssText).toBe('body { background-color: red; }');
+				expect(styleSheets[0].cssRules[1].cssText).toBe('div { background-color: green; }');
+				expect(styleSheets[1].cssRules.length).toBe(1);
+				expect(styleSheets[1].cssRules[0].cssText).toBe('button { background-color: red; }');
 
 				done();
 			}, 0);
diff --git a/packages/happy-dom/test/query-selector/QuerySelector.test.ts b/packages/happy-dom/test/query-selector/QuerySelector.test.ts
index a7864e16..47761118 100644
--- a/packages/happy-dom/test/query-selector/QuerySelector.test.ts
+++ b/packages/happy-dom/test/query-selector/QuerySelector.test.ts
@@ -21,16 +21,120 @@ describe('QuerySelector', () => {
 			container.innerHTML = QuerySelectorHTML;
 			const elements = container.querySelectorAll('span');
 			expect(elements.length).toBe(3);
-			expect(elements[0]).toBe(container.children[0].children[1].children[0]);
-			expect(elements[1]).toBe(container.children[0].children[1].children[1]);
-			expect(elements[2]).toBe(container.children[0].children[1].children[2]);
+			expect(elements[0] === container.children[0].children[1].children[0]).toBe(true);
+			expect(elements[1] === container.children[0].children[1].children[1]).toBe(true);
+			expect(elements[2] === container.children[0].children[1].children[2]).toBe(true);
+		});
+
+		it('Returns elements in document order.', () => {
+			const element = document.createElement('div');
+
+			element.innerHTML = `
+                <div class="a">
+                    <div class="aa">
+                        <div class="aaa"></div>
+                        <div class="aab"></div>
+                        <div class="aac"></div>
+                    </div>
+                    <div class="ab">
+                        <div class="aba"></div>
+                        <div class="abb"></div>
+                        <div class="abc"></div>
+                    </div>
+                    <div class="ac">
+                        <div class="aca"></div>
+                        <div class="acb"></div>
+                        <div class="acc"></div>
+                    </div>
+                </div>
+                <div class="b">
+                    <div class="ba">
+                        <div class="baa"></div>
+                        <div class="bab"></div>
+                        <div class="bac"></div>
+                    </div> 
+                    <div class="bb">
+                        <div class="bba"></div>
+                        <div class="bbb"></div>
+                        <div class="bbc"></div>
+                    </div>
+                    <div class="bc">
+                        <div class="bca"></div>
+                        <div class="bcb"></div>
+                        <div class="bcc"></div>
+                    </div>
+                </div>
+                <div class="c">
+                    <div class="ca">
+                        <div class="caa"></div>
+                        <div class="cab"></div>
+                        <div class="cac"></div>
+                    </div>
+                    <div class="cb">
+                        <div class="cba"></div>
+                        <div class="cbb"></div>
+                        <div class="cbc"></div>
+                    </div>
+                    <div class="cc">
+                        <div class="cca"></div>
+                        <div class="ccb"></div>
+                        <div class="ccc"></div>
+                    </div>
+                </div>
+            `;
+
+			expect(
+				Array.from(
+					element.querySelectorAll('div[class^="c"], div[class^="b"], div[class^="a"]')
+				).map((div) => div.className)
+			).toEqual([
+				'a',
+				'aa',
+				'aaa',
+				'aab',
+				'aac',
+				'ab',
+				'aba',
+				'abb',
+				'abc',
+				'ac',
+				'aca',
+				'acb',
+				'acc',
+				'b',
+				'ba',
+				'baa',
+				'bab',
+				'bac',
+				'bb',
+				'bba',
+				'bbb',
+				'bbc',
+				'bc',
+				'bca',
+				'bcb',
+				'bcc',
+				'c',
+				'ca',
+				'caa',
+				'cab',
+				'cac',
+				'cb',
+				'cba',
+				'cbb',
+				'cbc',
+				'cc',
+				'cca',
+				'ccb',
+				'ccc'
+			]);
 		});
 
 		it('Returns a NodeList with the method item().', () => {
 			const container = <IHTMLElement>document.createElement('div');
 			container.innerHTML = QuerySelectorHTML;
 			const elements = container.querySelectorAll('span');
-			expect(elements.item(0)).toBe(container.children[0].children[1].children[0]);
+			expect(elements.item(0) === container.children[0].children[1].children[0]).toBe(true);
 		});
 
 		it('Returns all h1 (heading 1) elements.', () => {
@@ -38,8 +142,8 @@ describe('QuerySelector', () => {
 			container.innerHTML = QuerySelectorHTML;
 			const elements = container.querySelectorAll('h1');
 			expect(elements.length).toBe(2);
-			expect(elements[0]).toBe(container.children[0].children[0]);
-			expect(elements[1]).toBe(container.children[1].children[0]);
+			expect(elements[0] === container.children[0].children[0]).toBe(true);
+			expect(elements[1] === container.children[1].children[0]).toBe(true);
 		});
 
 		it('Returns all elements with class name "class1".', () => {
@@ -47,11 +151,11 @@ describe('QuerySelector', () => {
 			container.innerHTML = QuerySelectorHTML;
 			const elements = container.querySelectorAll('.class1');
 			expect(elements.length).toBe(5);
-			expect(elements[0]).toBe(container.children[0]);
-			expect(elements[1]).toBe(container.children[0].children[1]);
-			expect(elements[2]).toBe(container.children[0].children[1].children[0]);
-			expect(elements[3]).toBe(container.children[0].children[1].children[1]);
-			expect(elements[4]).toBe(container.children[0].children[1].children[2]);
+			expect(elements[0] === container.children[0]).toBe(true);
+			expect(elements[1] === container.children[0].children[1]).toBe(true);
+			expect(elements[2] === container.children[0].children[1].children[0]).toBe(true);
+			expect(elements[3] === container.children[0].children[1].children[1]).toBe(true);
+			expect(elements[4] === container.children[0].children[1].children[2]).toBe(true);
 		});
 
 		it('Returns all elements with class name "before:after".', () => {
@@ -67,8 +171,8 @@ describe('QuerySelector', () => {
 			const validSelectorElements = container.querySelectorAll('.before\\:after');
 			expect(invalidSelectorElements.length).toBe(0);
 			expect(validSelectorElements.length).toBe(2);
-			expect(validSelectorElements[0]).toBe(element1);
-			expect(validSelectorElements[1]).toBe(element2);
+			expect(validSelectorElements[0] === element1).toBe(true);
+			expect(validSelectorElements[1] === element2).toBe(true);
 		});
 
 		it('Returns all elements with class name "before#after".', () => {
@@ -84,8 +188,8 @@ describe('QuerySelector', () => {
 			const validSelectorElements = container.querySelectorAll('.before\\#after');
 			expect(invalidSelectorElements.length).toBe(0);
 			expect(validSelectorElements.length).toBe(2);
-			expect(validSelectorElements[0]).toBe(element1);
-			expect(validSelectorElements[1]).toBe(element2);
+			expect(validSelectorElements[0] === element1).toBe(true);
+			expect(validSelectorElements[1] === element2).toBe(true);
 		});
 
 		it('Returns all elements with class name "before&after".', () => {
@@ -101,8 +205,8 @@ describe('QuerySelector', () => {
 			const validSelectorElements = container.querySelectorAll('.before\\&after');
 			expect(invalidSelectorElements.length).toBe(0);
 			expect(validSelectorElements.length).toBe(2);
-			expect(validSelectorElements[0]).toBe(element1);
-			expect(validSelectorElements[1]).toBe(element2);
+			expect(validSelectorElements[0] === element1).toBe(true);
+			expect(validSelectorElements[1] === element2).toBe(true);
 		});
 
 		it('Returns all elements with class name "class1 class2".', () => {
@@ -110,11 +214,11 @@ describe('QuerySelector', () => {
 			container.innerHTML = QuerySelectorHTML;
 			const elements = container.querySelectorAll('.class1.class2');
 			expect(elements.length).toBe(5);
-			expect(elements[0]).toBe(container.children[0]);
-			expect(elements[1]).toBe(container.children[0].children[1]);
-			expect(elements[2]).toBe(container.children[0].children[1].children[0]);
-			expect(elements[3]).toBe(container.children[0].children[1].children[1]);
-			expect(elements[4]).toBe(container.children[0].children[1].children[2]);
+			expect(elements[0] === container.children[0]).toBe(true);
+			expect(elements[1] === container.children[0].children[1]).toBe(true);
+			expect(elements[2] === container.children[0].children[1].children[0]).toBe(true);
+			expect(elements[3] === container.children[0].children[1].children[1]).toBe(true);
+			expect(elements[4] === container.children[0].children[1].children[2]).toBe(true);
 		});
 
 		it('Returns all elements matching ".class1 > .class1 > *".', () => {
@@ -122,9 +226,9 @@ describe('QuerySelector', () => {
 			container.innerHTML = QuerySelectorHTML;
 			const elements = container.querySelectorAll('.class1 > .class1 > *');
 			expect(elements.length).toBe(3);
-			expect(elements[0]).toBe(container.children[0].children[1].children[0]);
-			expect(elements[1]).toBe(container.children[0].children[1].children[1]);
-			expect(elements[2]).toBe(container.children[0].children[1].children[2]);
+			expect(elements[0] === container.children[0].children[1].children[0]).toBe(true);
+			expect(elements[1] === container.children[0].children[1].children[1]).toBe(true);
+			expect(elements[2] === container.children[0].children[1].children[2]).toBe(true);
 		});
 
 		it('Returns all elements matching "div > div > span".', () => {
@@ -132,9 +236,9 @@ describe('QuerySelector', () => {
 			container.innerHTML = QuerySelectorHTML;
 			const elements = container.querySelectorAll('div > div > span');
 			expect(elements.length).toBe(3);
-			expect(elements[0]).toBe(container.children[0].children[1].children[0]);
-			expect(elements[1]).toBe(container.children[0].children[1].children[1]);
-			expect(elements[2]).toBe(container.children[0].children[1].children[2]);
+			expect(elements[0] === container.children[0].children[1].children[0]).toBe(true);
+			expect(elements[1] === container.children[0].children[1].children[1]).toBe(true);
+			expect(elements[2] === container.children[0].children[1].children[2]).toBe(true);
 		});
 
 		it('Returns all elements matching "div > div > .class1.class2".', () => {
@@ -142,10 +246,10 @@ describe('QuerySelector', () => {
 			container.innerHTML = QuerySelectorHTML;
 			const elements = container.querySelectorAll('div > div > .class1.class2');
 			expect(elements.length).toBe(4);
-			expect(elements[0]).toBe(container.children[0].children[1]);
-			expect(elements[1]).toBe(container.children[0].children[1].children[0]);
-			expect(elements[2]).toBe(container.children[0].children[1].children[1]);
-			expect(elements[3]).toBe(container.children[0].children[1].children[2]);
+			expect(elements[0] === container.children[0].children[1]).toBe(true);
+			expect(elements[1] === container.children[0].children[1].children[0]).toBe(true);
+			expect(elements[2] === container.children[0].children[1].children[1]).toBe(true);
+			expect(elements[3] === container.children[0].children[1].children[2]).toBe(true);
 		});
 
 		it('Returns all elements with tag name and class "span.class1".', () => {
@@ -154,9 +258,36 @@ describe('QuerySelector', () => {
 			const elements = container.querySelectorAll('span.class1');
 
 			expect(elements.length).toBe(3);
-			expect(elements[0]).toBe(container.children[0].children[1].children[0]);
-			expect(elements[1]).toBe(container.children[0].children[1].children[1]);
-			expect(elements[2]).toBe(container.children[0].children[1].children[2]);
+			expect(elements[0] === container.children[0].children[1].children[0]).toBe(true);
+			expect(elements[1] === container.children[0].children[1].children[1]).toBe(true);
+			expect(elements[2] === container.children[0].children[1].children[2]).toBe(true);
+		});
+
+		it('Returns all elements with tag name and class ".a + .b".', () => {
+			const div = document.createElement('div');
+
+			div.innerHTML = `
+				<div class="a">a1</div>
+				<div class="b">b1</div>
+				<div class="c">c1</div>
+				<div class="a">a2</div>
+				<div class="b">b2</div>
+				<div class="a">a3</div>
+			`;
+
+			const firstDivB = div.querySelector('.a + .b');
+
+			expect(firstDivB === div.children[1]).toBe(true);
+
+			const allDivB = div.querySelectorAll('.a + .b');
+
+			expect(allDivB.length).toBe(2);
+			expect(allDivB[0].textContent).toBe('b1');
+			expect(allDivB[1].textContent).toBe('b2');
+
+			const firstDivC = div.querySelector('.a + .c');
+
+			expect(firstDivC === null).toBe(true);
 		});
 
 		it('Returns all elements with matching attributes using "[attr1="value1"]".', () => {
@@ -165,8 +296,8 @@ describe('QuerySelector', () => {
 			const elements = container.querySelectorAll('[attr1="value1"]');
 
 			expect(elements.length).toBe(2);
-			expect(elements[0]).toBe(container.children[0].children[1].children[0]);
-			expect(elements[1]).toBe(container.children[0].children[1].children[1]);
+			expect(elements[0] === container.children[0].children[1].children[0]).toBe(true);
+			expect(elements[1] === container.children[0].children[1].children[1]).toBe(true);
 		});
 
 		it('Returns all elements with matching attributes using "[attr1="word1.word2"]".', () => {
@@ -175,7 +306,7 @@ describe('QuerySelector', () => {
 			const elements = container.querySelectorAll('[attr1="word1.word2"]');
 
 			expect(elements.length).toBe(1);
-			expect(elements[0]).toBe(container.children[0].children[1].children[2]);
+			expect(elements[0] === container.children[0].children[1].children[2]).toBe(true);
 		});
 
 		it('Returns all elements with multiple matching attributes using "[attr1="value1"][attr2="word1 word2"]".', () => {
@@ -184,7 +315,7 @@ describe('QuerySelector', () => {
 			const elements = container.querySelectorAll('[attr1="value1"][attr2="word1 word2"]');
 
 			expect(elements.length).toBe(1);
-			expect(elements[0]).toBe(container.children[0].children[1].children[0]);
+			expect(elements[0] === container.children[0].children[1].children[0]).toBe(true);
 		});
 
 		it('Returns all elements with tag name and matching attributes using "span[attr1="value1"]".', () => {
@@ -193,8 +324,8 @@ describe('QuerySelector', () => {
 			const elements = container.querySelectorAll('span[attr1="value1"]');
 
 			expect(elements.length).toBe(2);
-			expect(elements[0]).toBe(container.children[0].children[1].children[0]);
-			expect(elements[1]).toBe(container.children[0].children[1].children[1]);
+			expect(elements[0] === container.children[0].children[1].children[0]).toBe(true);
+			expect(elements[1] === container.children[0].children[1].children[1]).toBe(true);
 		});
 
 		it('Returns all elements with tag name and matching attributes using "span[attr1=value1]".', () => {
@@ -203,8 +334,8 @@ describe('QuerySelector', () => {
 			const elements = container.querySelectorAll('span[attr1=value1]');
 
 			expect(elements.length).toBe(2);
-			expect(elements[0]).toBe(container.children[0].children[1].children[0]);
-			expect(elements[1]).toBe(container.children[0].children[1].children[1]);
+			expect(elements[0] === container.children[0].children[1].children[0]).toBe(true);
+			expect(elements[1] === container.children[0].children[1].children[1]).toBe(true);
 		});
 
 		it('Returns all elements with tag name and matching attributes using "span[attr1=\'value1\']".', () => {
@@ -213,8 +344,8 @@ describe('QuerySelector', () => {
 			const elements = container.querySelectorAll("span[attr1='value1']");
 
 			expect(elements.length).toBe(2);
-			expect(elements[0]).toBe(container.children[0].children[1].children[0]);
-			expect(elements[1]).toBe(container.children[0].children[1].children[1]);
+			expect(elements[0] === container.children[0].children[1].children[0]).toBe(true);
+			expect(elements[1] === container.children[0].children[1].children[1]).toBe(true);
 		});
 
 		it('Returns all elements with tag name and matching attributes using "span[_attr1]".', () => {
@@ -223,9 +354,9 @@ describe('QuerySelector', () => {
 			const elements = container.querySelectorAll('span[_attr1]');
 
 			expect(elements.length).toBe(3);
-			expect(elements[0]).toBe(container.children[0].children[1].children[0]);
-			expect(elements[1]).toBe(container.children[0].children[1].children[1]);
-			expect(elements[2]).toBe(container.children[0].children[1].children[2]);
+			expect(elements[0] === container.children[0].children[1].children[0]).toBe(true);
+			expect(elements[1] === container.children[0].children[1].children[1]).toBe(true);
+			expect(elements[2] === container.children[0].children[1].children[2]).toBe(true);
 		});
 
 		it('Returns all elements with tag name and multiple matching attributes using "span[attr1="value1"][attr2="word1 word2"]".', () => {
@@ -234,7 +365,7 @@ describe('QuerySelector', () => {
 			const elements = container.querySelectorAll('span[attr1="value1"][attr2="word1 word2"]');
 
 			expect(elements.length).toBe(1);
-			expect(elements[0]).toBe(container.children[0].children[1].children[0]);
+			expect(elements[0] === container.children[0].children[1].children[0]).toBe(true);
 		});
 
 		it('Returns all elements with tag name and multiple matching attributes using "span[attr1="value1"][attr3="bracket[]bracket"]".', () => {
@@ -243,7 +374,7 @@ describe('QuerySelector', () => {
 			const elements = container.querySelectorAll('span[attr1="value1"][attr3="bracket[]bracket"]');
 
 			expect(elements.length).toBe(1);
-			expect(elements[0]).toBe(container.children[0].children[1].children[0]);
+			expect(elements[0] === container.children[0].children[1].children[0]).toBe(true);
 		});
 
 		it('Returns all elements with tag name and multiple matching attributes using "span[attr1="application/ld+json"]".', () => {
@@ -255,8 +386,8 @@ describe('QuerySelector', () => {
 			const elements = container.querySelectorAll('span[attr1="application/ld+json"]');
 
 			expect(elements.length).toBe(2);
-			expect(elements[0]).toBe(container.children[0].children[1].children[0]);
-			expect(elements[1]).toBe(container.children[0].children[1].children[1]);
+			expect(elements[0] === container.children[0].children[1].children[0]).toBe(true);
+			expect(elements[1] === container.children[0].children[1].children[1]).toBe(true);
 		});
 
 		it('Returns all elements with an attribute value containing a specified word using "[class~="class2"]".', () => {
@@ -265,11 +396,11 @@ describe('QuerySelector', () => {
 			const elements = container.querySelectorAll('[class~="class2"]');
 
 			expect(elements.length).toBe(5);
-			expect(elements[0]).toBe(container.children[0]);
-			expect(elements[1]).toBe(container.children[0].children[1]);
-			expect(elements[2]).toBe(container.children[0].children[1].children[0]);
-			expect(elements[3]).toBe(container.children[0].children[1].children[1]);
-			expect(elements[4]).toBe(container.children[0].children[1].children[2]);
+			expect(elements[0] === container.children[0]).toBe(true);
+			expect(elements[1] === container.children[0].children[1]).toBe(true);
+			expect(elements[2] === container.children[0].children[1].children[0]).toBe(true);
+			expect(elements[3] === container.children[0].children[1].children[1]).toBe(true);
+			expect(elements[4] === container.children[0].children[1].children[2]).toBe(true);
 		});
 
 		it('Returns all elements with an attribute value starting with the specified word using "[class|="class1"]".', () => {
@@ -278,11 +409,11 @@ describe('QuerySelector', () => {
 			const elements = container.querySelectorAll('[class|="class1"]');
 
 			expect(elements.length).toBe(5);
-			expect(elements[0]).toBe(container.children[0]);
-			expect(elements[1]).toBe(container.children[0].children[1]);
-			expect(elements[2]).toBe(container.children[0].children[1].children[0]);
-			expect(elements[3]).toBe(container.children[0].children[1].children[1]);
-			expect(elements[4]).toBe(container.children[0].children[1].children[2]);
+			expect(elements[0] === container.children[0]).toBe(true);
+			expect(elements[1] === container.children[0].children[1]).toBe(true);
+			expect(elements[2] === container.children[0].children[1].children[0]).toBe(true);
+			expect(elements[3] === container.children[0].children[1].children[1]).toBe(true);
+			expect(elements[4] === container.children[0].children[1].children[2]).toBe(true);
 		});
 
 		it('Returns all elements with an attribute value that begins with a specified value using "[class^="cl"]".', () => {
@@ -291,11 +422,11 @@ describe('QuerySelector', () => {
 			const elements = container.querySelectorAll('[class^="cl"]');
 
 			expect(elements.length).toBe(5);
-			expect(elements[0]).toBe(container.children[0]);
-			expect(elements[1]).toBe(container.children[0].children[1]);
-			expect(elements[2]).toBe(container.children[0].children[1].children[0]);
-			expect(elements[3]).toBe(container.children[0].children[1].children[1]);
-			expect(elements[4]).toBe(container.children[0].children[1].children[2]);
+			expect(elements[0] === container.children[0]).toBe(true);
+			expect(elements[1] === container.children[0].children[1]).toBe(true);
+			expect(elements[2] === container.children[0].children[1].children[0]).toBe(true);
+			expect(elements[3] === container.children[0].children[1].children[1]).toBe(true);
+			expect(elements[4] === container.children[0].children[1].children[2]).toBe(true);
 		});
 
 		it('Returns all elements with an attribute value that ends with a specified value using "[class$="ss2"]".', () => {
@@ -304,11 +435,11 @@ describe('QuerySelector', () => {
 			const elements = container.querySelectorAll('[class$="ss2"]');
 
 			expect(elements.length).toBe(5);
-			expect(elements[0]).toBe(container.children[0]);
-			expect(elements[1]).toBe(container.children[0].children[1]);
-			expect(elements[2]).toBe(container.children[0].children[1].children[0]);
-			expect(elements[3]).toBe(container.children[0].children[1].children[1]);
-			expect(elements[4]).toBe(container.children[0].children[1].children[2]);
+			expect(elements[0] === container.children[0]).toBe(true);
+			expect(elements[1] === container.children[0].children[1]).toBe(true);
+			expect(elements[2] === container.children[0].children[1].children[0]).toBe(true);
+			expect(elements[3] === container.children[0].children[1].children[1]).toBe(true);
+			expect(elements[4] === container.children[0].children[1].children[2]).toBe(true);
 		});
 
 		it('Returns all elements with an attribute value that contains a specified value using "[class*="s1 cl"]".', () => {
@@ -330,11 +461,11 @@ describe('QuerySelector', () => {
 			const elements = container.querySelectorAll('[class*="s1 cl"], [attr1="value1"]');
 
 			expect(elements.length).toBe(5);
-			expect(elements[0]).toBe(container.children[0]);
-			expect(elements[1]).toBe(container.children[0].children[1]);
-			expect(elements[2]).toBe(container.children[0].children[1].children[0]);
-			expect(elements[3]).toBe(container.children[0].children[1].children[1]);
-			expect(elements[4]).toBe(container.children[0].children[1].children[2]);
+			expect(elements[0] === container.children[0]).toBe(true);
+			expect(elements[1] === container.children[0].children[1]).toBe(true);
+			expect(elements[2] === container.children[0].children[1].children[0]).toBe(true);
+			expect(elements[3] === container.children[0].children[1].children[1]).toBe(true);
+			expect(elements[4] === container.children[0].children[1].children[2]).toBe(true);
 		});
 
 		it('Returns all elements with an attribute value that contains a specified value using "[class*="s1 cl"]" or has the tag "b".', () => {
@@ -344,13 +475,13 @@ describe('QuerySelector', () => {
 			const children = container.children;
 			expect(children.length).toBe(2);
 			expect(elements.length).toBe(7);
-			expect(elements[0]).toBe(container.children[0]);
-			expect(elements[1]).toBe(container.children[0].children[1]);
-			expect(elements[2]).toBe(container.children[0].children[1].children[0]);
-			expect(elements[3]).toBe(container.children[0].children[1].children[1]);
-			expect(elements[4]).toBe(container.children[0].children[1].children[2]);
-			expect(elements[5]).toBe(container.children[0].children[0]);
-			expect(elements[6]).toBe(container.children[1].children[0]);
+			expect(elements[0] === container.children[0]).toBe(true);
+			expect(elements[1] === container.children[0].children[0]).toBe(true);
+			expect(elements[2] === container.children[0].children[1]).toBe(true);
+			expect(elements[3] === container.children[0].children[1].children[0]).toBe(true);
+			expect(elements[4] === container.children[0].children[1].children[1]).toBe(true);
+			expect(elements[5] === container.children[0].children[1].children[2]).toBe(true);
+			expect(elements[6] === container.children[1].children[0]).toBe(true);
 		});
 
 		it('Returns all span elements matching ":first-child".', () => {
@@ -359,10 +490,10 @@ describe('QuerySelector', () => {
 			const elements = container.querySelectorAll(':first-child');
 
 			expect(elements.length).toBe(4);
-			expect(elements[0]).toBe(container.children[0]);
-			expect(elements[1]).toBe(container.children[0].children[0]);
-			expect(elements[2]).toBe(container.children[0].children[1].children[0]);
-			expect(elements[3]).toBe(container.children[1].children[0]);
+			expect(elements[0] === container.children[0]).toBe(true);
+			expect(elements[1] === container.children[0].children[0]).toBe(true);
+			expect(elements[2] === container.children[0].children[1].children[0]).toBe(true);
+			expect(elements[3] === container.children[1].children[0]).toBe(true);
 		});
 
 		it('Returns all span elements matching "span:first-child".', () => {
@@ -371,7 +502,7 @@ describe('QuerySelector', () => {
 			const elements = container.querySelectorAll('span:first-child');
 
 			expect(elements.length).toBe(1);
-			expect(elements[0]).toBe(container.children[0].children[1].children[0]);
+			expect(elements[0] === container.children[0].children[1].children[0]).toBe(true);
 		});
 
 		it('Returns all span elements matching ":last-child".', () => {
@@ -380,10 +511,10 @@ describe('QuerySelector', () => {
 			const elements = container.querySelectorAll(':last-child');
 
 			expect(elements.length).toBe(4);
-			expect(elements[0]).toBe(container.children[0].children[1]);
-			expect(elements[1]).toBe(container.children[0].children[1].children[2]);
-			expect(elements[2]).toBe(container.children[1]);
-			expect(elements[3]).toBe(container.children[1].children[0]);
+			expect(elements[0] === container.children[0].children[1]).toBe(true);
+			expect(elements[1] === container.children[0].children[1].children[2]).toBe(true);
+			expect(elements[2] === container.children[1]).toBe(true);
+			expect(elements[3] === container.children[1].children[0]).toBe(true);
 		});
 
 		it('Returns all span elements matching "span:last-child".', () => {
@@ -392,7 +523,7 @@ describe('QuerySelector', () => {
 			const elements = container.querySelectorAll('span:last-child');
 
 			expect(elements.length).toBe(1);
-			expect(elements[0]).toBe(container.children[0].children[1].children[2]);
+			expect(elements[0] === container.children[0].children[1].children[2]).toBe(true);
 		});
 
 		it('Returns all span elements matching ":only-child".', () => {
@@ -410,11 +541,11 @@ describe('QuerySelector', () => {
 			const elements = container.querySelectorAll(':first-of-type');
 
 			expect(elements.length).toBe(5);
-			expect(elements[0]).toBe(container.children[0]);
-			expect(elements[1]).toBe(container.children[0].children[0]);
-			expect(elements[2]).toBe(container.children[0].children[1]);
-			expect(elements[3]).toBe(container.children[0].children[1].children[0]);
-			expect(elements[4]).toBe(container.children[1].children[0]);
+			expect(elements[0] === container.children[0]).toBe(true);
+			expect(elements[1] === container.children[0].children[0]).toBe(true);
+			expect(elements[2] === container.children[0].children[1]).toBe(true);
+			expect(elements[3] === container.children[0].children[1].children[0]).toBe(true);
+			expect(elements[4] === container.children[1].children[0]).toBe(true);
 		});
 
 		it('Returns all span elements matching "span:first-of-type".', () => {
@@ -423,7 +554,7 @@ describe('QuerySelector', () => {
 			const elements = container.querySelectorAll('span:first-of-type');
 
 			expect(elements.length).toBe(1);
-			expect(elements[0]).toBe(container.children[0].children[1].children[0]);
+			expect(elements[0] === container.children[0].children[1].children[0]).toBe(true);
 		});
 
 		it('Returns all span elements matching ":last-of-type".', () => {
@@ -432,11 +563,11 @@ describe('QuerySelector', () => {
 			const elements = container.querySelectorAll(':last-of-type');
 
 			expect(elements.length).toBe(5);
-			expect(elements[0]).toBe(container.children[0].children[0]);
-			expect(elements[1]).toBe(container.children[0].children[1]);
-			expect(elements[2]).toBe(container.children[0].children[1].children[2]);
-			expect(elements[3]).toBe(container.children[1]);
-			expect(elements[4]).toBe(container.children[1].children[0]);
+			expect(elements[0] === container.children[0].children[0]).toBe(true);
+			expect(elements[1] === container.children[0].children[1]).toBe(true);
+			expect(elements[2] === container.children[0].children[1].children[2]).toBe(true);
+			expect(elements[3] === container.children[1]).toBe(true);
+			expect(elements[4] === container.children[1].children[0]).toBe(true);
 		});
 
 		it('Returns all input elements matching "input[name="op"]:checked".', () => {
@@ -449,15 +580,21 @@ describe('QuerySelector', () => {
 			</form>
 			`;
 			let elements = container.querySelectorAll('input[name="op"]:checked');
+
 			expect(elements.length).toBe(1);
-			expect(elements[0]).toBe(container.children[0].children[0]);
+			expect(elements[0] === container.children[0].children[0]).toBe(true);
+
 			const input = <IHTMLInputElement>elements[0];
+
 			expect(input.value).toBe('one');
+
 			const twoEl = <IHTMLInputElement>container.querySelector("input[value='two']");
+
 			twoEl.checked = true;
 			elements = container.querySelectorAll('input[name="op"]:checked');
+
 			expect(elements.length).toBe(1);
-			expect(elements[0]).toBe(container.children[0].children[1]);
+			expect(elements[0] === container.children[0].children[1]).toBe(true);
 			expect((<IHTMLInputElement>elements[0]).value).toBe('two');
 		});
 
@@ -467,8 +604,8 @@ describe('QuerySelector', () => {
 			const elements = container.querySelectorAll('span:not([type=hidden])');
 
 			expect(elements.length).toBe(2);
-			expect(elements[0]).toBe(container.children[0].children[1].children[1]);
-			expect(elements[1]).toBe(container.children[0].children[1].children[2]);
+			expect(elements[0] === container.children[0].children[1].children[1]).toBe(true);
+			expect(elements[1] === container.children[0].children[1].children[2]).toBe(true);
 		});
 
 		it('Returns all elements matching ".foo:not(.bar)".', () => {
@@ -481,7 +618,7 @@ describe('QuerySelector', () => {
 			const elements = container.querySelectorAll('.foo:not(.bar)');
 
 			expect(elements.length).toBe(1);
-			expect(elements[0]).toBe(container.children[1]);
+			expect(elements[0] === container.children[1]).toBe(true);
 		});
 
 		it('Returns all elements matching ".bar:not(.foo)".', () => {
@@ -494,7 +631,7 @@ describe('QuerySelector', () => {
 			const elements = container.querySelectorAll('.bar:not(.foo)');
 
 			expect(elements.length).toBe(1);
-			expect(elements[0]).toBe(container.children[2]);
+			expect(elements[0] === container.children[2]).toBe(true);
 		});
 
 		it('Returns all elements matching "[data-foo]:not([data-bar])".', () => {
@@ -506,7 +643,7 @@ describe('QuerySelector', () => {
 			const elements = document.querySelectorAll('[data-foo]:not([data-bar])');
 
 			expect(elements.length).toBe(1);
-			expect(elements[0]).toBe(document.body.children[1]);
+			expect(elements[0] === document.body.children[1]).toBe(true);
 		});
 
 		it('Returns all elements matching "[tabindex]:not(textarea)".', () => {
@@ -518,7 +655,7 @@ describe('QuerySelector', () => {
 			`;
 			const elements = document.querySelectorAll('[tabindex]:not(textarea)');
 			expect(elements.length).toBe(3);
-			expect(elements[0]).toBe(document.body.children[0]);
+			expect(elements[0] === document.body.children[0]).toBe(true);
 		});
 
 		it('Returns all span elements matching span:nth-child(1) or span:nth-child(2).', () => {
@@ -527,8 +664,8 @@ describe('QuerySelector', () => {
 			const elements = container.querySelectorAll('span:nth-child(1), span:nth-child(2)');
 
 			expect(elements.length).toBe(2);
-			expect(elements[0]).toBe(container.children[0].children[1].children[0]);
-			expect(elements[1]).toBe(container.children[0].children[1].children[1]);
+			expect(elements[0] === container.children[0].children[1].children[0]).toBe(true);
+			expect(elements[1] === container.children[0].children[1].children[1]).toBe(true);
 		});
 
 		it('Returns all elements matching ":nth-child(n+8)".', () => {