Repository: capricorn86/happy-dom
Pull request #314 (2021-12-17 00:34:08)

#313@minor: Adds support for EventTarget.attachEvent. Adds support fo…
…r PointerEvent. Adds support for Document.activeElement. Adds support for ShadowRoot.activeElemnet. Improves performance of Node.getRootNode(). Adds support for Element.closest(). Adds support for checking against multiple selectors divided by comma to Element.matches(). Fixes issue with composed events not bubbling through ShadowRoot. Corrects event types in HTMLElement.click(), HTMLElement.focus(), HTMLElement.blur(). Fixes issue with HTMLInputElement.defaultValue being undefined. Fixes issue with query selectors not supporting the :not() pseudo selector.


Patch:
diff --git a/packages/happy-dom/package.json b/packages/happy-dom/package.json
index 02a06d36..08716b1a 100644
--- a/packages/happy-dom/package.json
+++ b/packages/happy-dom/package.json
@@ -27,7 +27,7 @@
 		"watch": "tsc -w --preserveWatchOutput",
 		"lint": "eslint --ignore-path .gitignore --max-warnings 0 .",
 		"lint:fix": "eslint --ignore-path .gitignore --max-warnings 0 --fix .",
-		"test": "jest",
+		"test": "jest --runInBand",
 		"test:coverage": "jest --collectCoverage",
 		"test:watch": "jest --watch",
 		"test:update-snapshot": "jest --updateSnapshot",
diff --git a/packages/happy-dom/src/event/EventTarget.ts b/packages/happy-dom/src/event/EventTarget.ts
index 3fdcb3e0..88e6cc9f 100644
--- a/packages/happy-dom/src/event/EventTarget.ts
+++ b/packages/happy-dom/src/event/EventTarget.ts
@@ -67,4 +67,16 @@ export default abstract class EventTarget implements IEventTarget {
 
 		return !(event.cancelable && event.defaultPrevented);
 	}
+
+	/**
+	 * Adds an event listener.
+	 *
+	 * This is only supported by IE8- and Opera, but for some reason React uses it and calls it, so therefore we will keep support for it until they stop using it.
+	 *
+	 * @param type Event type.
+	 * @param listener Listener.
+	 */
+	public attachEvent(type: string, listener: ((event: Event) => void) | IEventListener): void {
+		this.addEventListener(type.replace('on', ''), listener);
+	}
 }
diff --git a/packages/happy-dom/src/event/NonImplementedEventTypes.ts b/packages/happy-dom/src/event/NonImplementedEventTypes.ts
index eccbe72a..123d7351 100644
--- a/packages/happy-dom/src/event/NonImplementedEventTypes.ts
+++ b/packages/happy-dom/src/event/NonImplementedEventTypes.ts
@@ -25,7 +25,6 @@ export default [
 	'OverconstrainedError',
 	'PageTransitionEvent',
 	'PaymentRequestUpdateEvent',
-	'PointerEvent',
 	'PopStateEvent',
 	'ProgressEvent',
 	'RelatedEvent',
diff --git a/packages/happy-dom/src/event/events/IPointerEventInit.ts b/packages/happy-dom/src/event/events/IPointerEventInit.ts
new file mode 100644
index 00000000..56be501c
--- /dev/null
+++ b/packages/happy-dom/src/event/events/IPointerEventInit.ts
@@ -0,0 +1,14 @@
+import IMouseEventInit from './IMouseEventInit';
+
+export default interface IPointerEventInit extends IMouseEventInit {
+	pointerId?: number;
+	width?: number;
+	height?: number;
+	pressure?: number;
+	tangentialPressure?: number;
+	tiltX?: number;
+	tiltY?: number;
+	twist?: number;
+	pointerType?: string;
+	isPrimary?: boolean;
+}
diff --git a/packages/happy-dom/src/event/events/PointerEvent.ts b/packages/happy-dom/src/event/events/PointerEvent.ts
new file mode 100644
index 00000000..d10ce05f
--- /dev/null
+++ b/packages/happy-dom/src/event/events/PointerEvent.ts
@@ -0,0 +1,42 @@
+import MouseEvent from './MouseEvent';
+import IPointerEventInit from './IPointerEventInit';
+
+/**
+ *
+ */
+export default class PointerEvent extends MouseEvent {
+	public readonly pointerId: number = 0;
+	public readonly width: number = 0;
+	public readonly height: number = 0;
+	public readonly pressure: number = 0;
+	public readonly tangentialPressure: number = 0;
+	public readonly tiltX: number = 0;
+	public readonly tiltY: number = 0;
+	public readonly twist: number = 0;
+	public readonly pointerType: string = '';
+	public readonly isPrimary: boolean = false;
+
+	/**
+	 * Constructor.
+	 *
+	 * @param type Event type.
+	 * @param [eventInit] Event init.
+	 */
+	constructor(type: string, eventInit: IPointerEventInit = null) {
+		super(type, eventInit);
+
+		if (eventInit) {
+			this.pointerId = eventInit.pointerId !== undefined ? eventInit.pointerId : 0;
+			this.width = eventInit.width !== undefined ? eventInit.width : 0;
+			this.height = eventInit.height !== undefined ? eventInit.height : 0;
+			this.pressure = eventInit.pressure !== undefined ? eventInit.pressure : 0;
+			this.tangentialPressure =
+				eventInit.tangentialPressure !== undefined ? eventInit.tangentialPressure : 0;
+			this.tiltX = eventInit.tiltX !== undefined ? eventInit.tiltX : 0;
+			this.tiltY = eventInit.tiltY !== undefined ? eventInit.tiltY : 0;
+			this.twist = eventInit.twist !== undefined ? eventInit.twist : 0;
+			this.pointerType = eventInit.pointerType !== undefined ? eventInit.pointerType : '';
+			this.isPrimary = eventInit.isPrimary || eventInit.isPrimary;
+		}
+	}
+}
diff --git a/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts b/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts
index 321f9ad6..28e82b62 100644
--- a/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts
+++ b/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts
@@ -13,6 +13,7 @@ import HTMLCollectionFactory from '../element/HTMLCollectionFactory';
 export default class DocumentFragment extends Node implements IDocumentFragment {
 	public nodeType = Node.DOCUMENT_FRAGMENT_NODE;
 	public readonly children: IHTMLCollection<IElement> = HTMLCollectionFactory.create();
+	public _rootNode: INode = this;
 
 	/**
 	 * Last element child.
diff --git a/packages/happy-dom/src/nodes/document/Document.ts b/packages/happy-dom/src/nodes/document/Document.ts
index 0c8759fa..739e5bf1 100644
--- a/packages/happy-dom/src/nodes/document/Document.ts
+++ b/packages/happy-dom/src/nodes/document/Document.ts
@@ -44,8 +44,9 @@ export default class Document extends Node implements IDocument {
 	public implementation: DOMImplementation;
 	public readonly children: IHTMLCollection<IElement> = HTMLCollectionFactory.create();
 	public readonly readyState = DocumentReadyStateEnum.interactive;
+	public readonly isConnected: boolean = true;
 	public _readyStateManager: DocumentReadyStateManager = null;
-	protected _isConnected = true;
+	public _activeElement: IHTMLElement = null;
 	protected _isFirstWrite = true;
 	protected _isFirstWriteAfterOpen = false;
 	private _defaultView: Window = null;
@@ -209,6 +210,15 @@ export default class Document extends Node implements IDocument {
 		return styleSheets;
 	}
 
+	/**
+	 * Returns active element.
+	 *
+	 * @returns Active element.
+	 */
+	public get activeElement(): IHTMLElement {
+		return this._activeElement || this.body || this.documentElement || null;
+	}
+
 	/**
 	 * Inserts a set of Node objects or DOMString objects after the last child of the ParentNode. DOMString objects are inserted as equivalent Text nodes.
 	 *
diff --git a/packages/happy-dom/src/nodes/document/IDocument.ts b/packages/happy-dom/src/nodes/document/IDocument.ts
index 106b2f80..da4dbbbe 100644
--- a/packages/happy-dom/src/nodes/document/IDocument.ts
+++ b/packages/happy-dom/src/nodes/document/IDocument.ts
@@ -23,6 +23,7 @@ export default interface IDocument extends IParentNode {
 	readonly doctype: IDocumentType;
 	readonly body: IHTMLElement;
 	readonly head: IHTMLElement;
+	readonly activeElement: IHTMLElement;
 
 	/**
 	 * Replaces the document HTML with new HTML.
diff --git a/packages/happy-dom/src/nodes/element/Element.ts b/packages/happy-dom/src/nodes/element/Element.ts
index 745a1277..7aad40d3 100644
--- a/packages/happy-dom/src/nodes/element/Element.ts
+++ b/packages/happy-dom/src/nodes/element/Element.ts
@@ -598,7 +598,7 @@ export default class Element extends Node implements IElement {
 		(<IDocument>this.shadowRoot.ownerDocument) = this.ownerDocument;
 		(<Element>this.shadowRoot.host) = this;
 		(<string>this.shadowRoot.mode) = shadowRootInit.mode;
-		this.shadowRoot.isConnected = this.isConnected;
+		(<ShadowRoot>this.shadowRoot)._connectToNode(this);
 		return this.shadowRoot;
 	}
 
@@ -636,7 +636,46 @@ export default class Element extends Node implements IElement {
 	 * @returns "true" if matching.
 	 */
 	public matches(selector: string): boolean {
-		return new SelectorItem(selector).match(this);
+		for (const part of selector.split(',')) {
+			if (new SelectorItem(part.trim()).match(this)) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	/**
+	 * Traverses the Element and its parents (heading toward the document root) until it finds a node that matches the provided selector string.
+	 *
+	 * @param selector Selector.
+	 * @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)) {
+				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/element/IElement.ts b/packages/happy-dom/src/nodes/element/IElement.ts
index a6ce2634..b5edeed3 100644
--- a/packages/happy-dom/src/nodes/element/IElement.ts
+++ b/packages/happy-dom/src/nodes/element/IElement.ts
@@ -152,6 +152,14 @@ export default interface IElement extends IChildNode, INonDocumentTypeChildNode,
 	 */
 	matches(selector: string): boolean;
 
+	/**
+	 * Traverses the Element and its parents (heading toward the document root) until it finds a node that matches the provided selector string.
+	 *
+	 * @param selector Selector.
+	 * @returns Closest matching element.
+	 */
+	closest(selector: string): IElement;
+
 	/**
 	 * The setAttributeNode() method adds a new Attr node to the specified element.
 	 *
diff --git a/packages/happy-dom/src/nodes/html-element/HTMLElement.ts b/packages/happy-dom/src/nodes/html-element/HTMLElement.ts
index 722fce30..1ed1a9a9 100644
--- a/packages/happy-dom/src/nodes/html-element/HTMLElement.ts
+++ b/packages/happy-dom/src/nodes/html-element/HTMLElement.ts
@@ -1,8 +1,9 @@
 import Element from '../element/Element';
-import Event from '../../event/Event';
 import IHTMLElement from './IHTMLElement';
 import CSSStyleDeclaration from '../../css/CSSStyleDeclaration';
 import Attr from '../../attribute/Attr';
+import FocusEvent from '../../event/events/FocusEvent';
+import PointerEvent from '../../event/events/PointerEvent';
 
 /**
  * HTML Element.
@@ -172,7 +173,7 @@ export default class HTMLElement extends Element implements IHTMLElement {
 	 * Triggers a click event.
 	 */
 	public click(): void {
-		const event = new Event('click', {
+		const event = new PointerEvent('click', {
 			bubbles: true,
 			composed: true
 		});
@@ -185,8 +186,14 @@ export default class HTMLElement extends Element implements IHTMLElement {
 	 * Triggers a blur event.
 	 */
 	public blur(): void {
+		if (this.ownerDocument['_activeElement'] !== this || !this.isConnected) {
+			return;
+		}
+
+		this.ownerDocument['_activeElement'] = null;
+
 		for (const eventType of ['blur', 'focusout']) {
-			const event = new Event(eventType, {
+			const event = new FocusEvent(eventType, {
 				bubbles: true,
 				composed: true
 			});
@@ -200,8 +207,18 @@ export default class HTMLElement extends Element implements IHTMLElement {
 	 * Triggers a focus event.
 	 */
 	public focus(): void {
+		if (this.ownerDocument['_activeElement'] === this || !this.isConnected) {
+			return;
+		}
+
+		if (this.ownerDocument['_activeElement'] !== null) {
+			this.ownerDocument['_activeElement'].blur();
+		}
+
+		this.ownerDocument['_activeElement'] = this;
+
 		for (const eventType of ['focus', 'focusin']) {
-			const event = new Event(eventType, {
+			const event = new FocusEvent(eventType, {
 				bubbles: true,
 				composed: true
 			});
diff --git a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts
index 06e076cc..f11f828a 100644
--- a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts
+++ b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts
@@ -37,9 +37,6 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE
 	// Type specific: file
 	public files: File[] = [];
 
-	// Not categorized
-	public defaultValue = '';
-
 	// Type specific: text/password/search/tel/url/week/month
 	private _selectionStart = null;
 	private _selectionEnd = null;
@@ -384,21 +381,21 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE
 	}
 
 	/**
-	 * Returns defaultvalue.
+	 * Returns defaultValue.
 	 *
 	 * @returns Defaultvalue.
 	 */
-	public get defaultvalue(): string {
+	public get defaultValue(): string {
 		return this.getAttributeNS(null, 'defaultvalue') || '';
 	}
 
 	/**
-	 * Sets defaultvalue.
+	 * Sets defaultValue.
 	 *
-	 * @param defaultvalue Defaultvalue.
+	 * @param defaultValue Defaultvalue.
 	 */
-	public set defaultvalue(defaultvalue: string) {
-		this.setAttributeNS(null, 'defaultvalue', defaultvalue);
+	public set defaultValue(defaultValue: string) {
+		this.setAttributeNS(null, 'defaultvalue', defaultValue);
 	}
 
 	/**
@@ -780,6 +777,21 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE
 		return this.value ? parseFloat(this.value) : NaN;
 	}
 
+	/**
+	 * Selects the text.
+	 */
+	public select(): void {
+		if (!this._isSelectionSupported()) {
+			return null;
+		}
+
+		this._selectionStart = 0;
+		this._selectionEnd = this.value.length;
+		this._selectionDirection = HTMLInputElementSelectionDirectionEnum.none;
+
+		this.dispatchEvent(new Event('select', { bubbles: true, cancelable: true }));
+	}
+
 	/**
 	 * Set selection range.
 	 *
diff --git a/packages/happy-dom/src/nodes/html-input-element/IHTMLInputElement.ts b/packages/happy-dom/src/nodes/html-input-element/IHTMLInputElement.ts
index 6b997a3a..cd896aa9 100644
--- a/packages/happy-dom/src/nodes/html-input-element/IHTMLInputElement.ts
+++ b/packages/happy-dom/src/nodes/html-input-element/IHTMLInputElement.ts
@@ -35,7 +35,6 @@ export default interface IHTMLInputElement extends IHTMLElement {
 	allowdirs: string;
 	autocomplete: string;
 	src: string;
-	defaultvalue: string;
 	readOnly: boolean;
 	disabled: boolean;
 	autofocus: boolean;
diff --git a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts
index fe674ac8..9c135474 100644
--- a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts
+++ b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts
@@ -6,6 +6,7 @@ import Document from '../document/Document';
 import IHTMLLinkElement from './IHTMLLinkElement';
 import Event from '../../event/Event';
 import ErrorEvent from '../../event/events/ErrorEvent';
+import INode from '../../nodes/node/INode';
 
 /**
  * HTML Link Element.
@@ -19,79 +20,6 @@ export default class HTMLLinkElement extends HTMLElement implements IHTMLLinkEle
 	public readonly sheet: CSSStyleSheet = null;
 	public _evaluateCSS = true;
 
-	/**
-	 * Returns "true" if connected to DOM.
-	 *
-	 * @returns "true" if connected.
-	 */
-	public get isConnected(): boolean {
-		return this._isConnected;
-	}
-
-	/**
-	 * Sets the connected state.
-	 *
-	 * @param isConnected "true" if connected.
-	 */
-	public set isConnected(isConnected) {
-		if (this._isConnected !== isConnected) {
-			this._isConnected = isConnected;
-
-			for (const child of this.childNodes) {
-				child.isConnected = isConnected;
-			}
-
-			// eslint-disable-next-line
-			if (this.shadowRoot) {
-				// eslint-disable-next-line
-				this.shadowRoot.isConnected = isConnected;
-			}
-
-			if (isConnected && this._evaluateCSS) {
-				const href = this.getAttributeNS(null, 'href');
-				const rel = this.getAttributeNS(null, 'rel');
-				if (href !== null && rel && rel.toLowerCase() === 'stylesheet') {
-					(<Document>this.ownerDocument)._readyStateManager.startTask();
-					ResourceFetcher.fetch({ window: this.ownerDocument.defaultView, url: href })
-						.then(code => {
-							const styleSheet = new CSSStyleSheet();
-							styleSheet.replaceSync(code);
-							(<CSSStyleSheet>this.sheet) = styleSheet;
-							this.dispatchEvent(new Event('load'));
-							(<Document>this.ownerDocument)._readyStateManager.endTask();
-						})
-						.catch(error => {
-							this.dispatchEvent(
-								new ErrorEvent('error', {
-									message: error.message,
-									error
-								})
-							);
-							this.ownerDocument.defaultView.dispatchEvent(
-								new ErrorEvent('error', {
-									message: error.message,
-									error
-								})
-							);
-							(<Document>this.ownerDocument)._readyStateManager.endTask();
-							if (
-								!this._listeners['error'] &&
-								!this.ownerDocument.defaultView._listeners['error']
-							) {
-								this.ownerDocument.defaultView.console.error(error);
-							}
-						});
-				}
-			}
-
-			if (isConnected && this.connectedCallback) {
-				this.connectedCallback();
-			} else if (!isConnected && this.disconnectedCallback) {
-				this.disconnectedCallback();
-			}
-		}
-	}
-
 	/**
 	 * Returns as.
 	 *
@@ -286,4 +214,49 @@ export default class HTMLLinkElement extends HTMLElement implements IHTMLLinkEle
 
 		return replacedAttribute;
 	}
+
+	/**
+	 * @override
+	 */
+	public _connectToNode(parentNode: INode = null): void {
+		const isConnected = this.isConnected;
+		const isParentConnected = parentNode ? parentNode.isConnected : false;
+
+		super._connectToNode(parentNode);
+
+		if (isConnected !== isParentConnected && this._evaluateCSS) {
+			const href = this.getAttributeNS(null, 'href');
+			const rel = this.getAttributeNS(null, 'rel');
+
+			if (href !== null && rel && rel.toLowerCase() === 'stylesheet') {
+				(<Document>this.ownerDocument)._readyStateManager.startTask();
+				ResourceFetcher.fetch({ window: this.ownerDocument.defaultView, url: href })
+					.then(code => {
+						const styleSheet = new CSSStyleSheet();
+						styleSheet.replaceSync(code);
+						(<CSSStyleSheet>this.sheet) = styleSheet;
+						this.dispatchEvent(new Event('load'));
+						(<Document>this.ownerDocument)._readyStateManager.endTask();
+					})
+					.catch(error => {
+						this.dispatchEvent(
+							new ErrorEvent('error', {
+								message: error.message,
+								error
+							})
+						);
+						this.ownerDocument.defaultView.dispatchEvent(
+							new ErrorEvent('error', {
+								message: error.message,
+								error
+							})
+						);
+						(<Document>this.ownerDocument)._readyStateManager.endTask();
+						if (!this._listeners['error'] && !this.ownerDocument.defaultView._listeners['error']) {
+							this.ownerDocument.defaultView.console.error(error);
+						}
+					});
+			}
+		}
+	}
 }
diff --git a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts
index fddc1d2e..1b7de06b 100644
--- a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts
+++ b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts
@@ -4,6 +4,7 @@ import IHTMLScriptElement from './IHTMLScriptElement';
 import ScriptUtility from './ScriptUtility';
 import Event from '../../event/Event';
 import ErrorEvent from '../../event/events/ErrorEvent';
+import INode from '../../nodes/node/INode';
 
 /**
  * HTML Script Element.
@@ -16,55 +17,6 @@ export default class HTMLScriptElement extends HTMLElement implements IHTMLScrip
 	public onload: (event: Event) => void = null;
 	public _evaluateScript = true;
 
-	/**
-	 * Returns "true" if connected to DOM.
-	 *
-	 * @returns "true" if connected.
-	 */
-	public get isConnected(): boolean {
-		return this._isConnected;
-	}
-
-	/**
-	 * Sets the connected state.
-	 *
-	 * @param isConnected "true" if connected.
-	 */
-	public set isConnected(isConnected) {
-		if (this._isConnected !== isConnected) {
-			this._isConnected = isConnected;
-
-			for (const child of this.childNodes) {
-				child.isConnected = isConnected;
-			}
-
-			// eslint-disable-next-line
-			if (this.shadowRoot) {
-				// eslint-disable-next-line
-				this.shadowRoot.isConnected = isConnected;
-			}
-
-			if (isConnected && this._evaluateScript) {
-				const src = this.getAttributeNS(null, 'src');
-
-				if (src !== null) {
-					ScriptUtility.loadExternalScript(this);
-				} else {
-					const textContent = this.textContent;
-					if (textContent) {
-						this.ownerDocument.defaultView.eval(textContent);
-					}
-				}
-			}
-
-			if (isConnected && this.connectedCallback) {
-				this.connectedCallback();
-			} else if (!isConnected && this.disconnectedCallback) {
-				this.disconnectedCallback();
-			}
-		}
-	}
-
 	/**
 	 * Returns type.
 	 *
@@ -226,4 +178,27 @@ export default class HTMLScriptElement extends HTMLElement implements IHTMLScrip
 	public cloneNode(deep = false): IHTMLScriptElement {
 		return <IHTMLScriptElement>super.cloneNode(deep);
 	}
+
+	/**
+	 * @override
+	 */
+	public _connectToNode(parentNode: INode = null): void {
+		const isConnected = this.isConnected;
+		const isParentConnected = parentNode ? parentNode.isConnected : false;
+
+		super._connectToNode(parentNode);
+
+		if (isConnected !== isParentConnected && this._evaluateScript) {
+			const src = this.getAttributeNS(null, 'src');
+
+			if (src !== null) {
+				ScriptUtility.loadExternalScript(this);
+			} else {
+				const textContent = this.textContent;
+				if (textContent) {
+					this.ownerDocument.defaultView.eval(textContent);
+				}
+			}
+		}
+	}
 }
diff --git a/packages/happy-dom/src/nodes/html-style-element/HTMLStyleElement.ts b/packages/happy-dom/src/nodes/html-style-element/HTMLStyleElement.ts
index 1921b1cb..793db9b5 100644
--- a/packages/happy-dom/src/nodes/html-style-element/HTMLStyleElement.ts
+++ b/packages/happy-dom/src/nodes/html-style-element/HTMLStyleElement.ts
@@ -15,7 +15,7 @@ export default class HTMLStyleElement extends HTMLElement implements IHTMLStyleE
 	 * @returns CSS style sheet.
 	 */
 	public get sheet(): CSSStyleSheet {
-		if (!this._isConnected) {
+		if (!this.isConnected) {
 			return null;
 		}
 		const styleSheet = new CSSStyleSheet();
diff --git a/packages/happy-dom/src/nodes/node/INode.ts b/packages/happy-dom/src/nodes/node/INode.ts
index c7fe1e8f..8ecdf272 100644
--- a/packages/happy-dom/src/nodes/node/INode.ts
+++ b/packages/happy-dom/src/nodes/node/INode.ts
@@ -8,7 +8,7 @@ export default interface INode extends IEventTarget {
 	readonly parentElement: IElement;
 	readonly nodeType: number;
 	readonly childNodes: INode[];
-	isConnected: boolean;
+	readonly isConnected: boolean;
 	readonly nodeValue: string;
 	readonly nodeName: string;
 	readonly previousSibling: INode;
diff --git a/packages/happy-dom/src/nodes/node/Node.ts b/packages/happy-dom/src/nodes/node/Node.ts
index 4937e76d..34578b77 100644
--- a/packages/happy-dom/src/nodes/node/Node.ts
+++ b/packages/happy-dom/src/nodes/node/Node.ts
@@ -9,7 +9,6 @@ import IDocument from '../document/IDocument';
 import IElement from '../element/IElement';
 import INodeList from './INodeList';
 import NodeListFactory from './NodeListFactory';
-import { IShadowRoot } from '../..';
 
 /**
  * Node.
@@ -27,9 +26,8 @@ export default class Node extends EventTarget implements INode {
 	public readonly parentNode: INode = null;
 	public readonly nodeType: number;
 	public readonly childNodes: INodeList<INode> = NodeListFactory.create();
-
-	// Protected properties
-	protected _isConnected = false;
+	public readonly isConnected: boolean = false;
+	public _rootNode: INode = null;
 
 	// Custom Properties (not part of HTML standard)
 	protected _observers: MutationObserverListener[] = [];
@@ -42,42 +40,6 @@ export default class Node extends EventTarget implements INode {
 		this.ownerDocument = (<typeof Node>this.constructor).ownerDocument;
 	}
 
-	/**
-	 * Returns "true" if connected to DOM.
-	 *
-	 * @returns "true" if connected.
-	 */
-	public get isConnected(): boolean {
-		return this._isConnected;
-	}
-
-	/**
-	 * Sets the connected state.
-	 *
-	 * @param isConnected "true" if connected.
-	 */
-	public set isConnected(isConnected) {
-		if (this._isConnected !== isConnected) {
-			this._isConnected = isConnected;
-
-			if (isConnected && this.connectedCallback) {
-				this.connectedCallback();
-			} else if (!isConnected && this.disconnectedCallback) {
-				this.disconnectedCallback();
-			}
-
-			for (const child of this.childNodes) {
-				child.isConnected = isConnected;
-			}
-
-			// eslint-disable-next-line
-			if ((<any>this).shadowRoot) {
-				// eslint-disable-next-line
-				(<any>this).shadowRoot.isConnected = isConnected;
-			}
-		}
-	}
-
 	/**
 	 * Get text value of children.
 	 *
@@ -208,21 +170,15 @@ export default class Node extends EventTarget implements INode {
 	 * @returns Node.
 	 */
 	public getRootNode(options?: { composed: boolean }): INode {
-		// eslint-disable-next-line
-        let parent: INode = this;
+		if (!this.isConnected) {
+			return this;
+		}
 
-		while (!!parent) {
-			if (!parent.parentNode) {
-				if (!options?.composed || !(<IShadowRoot>parent).host) {
-					return parent;
-				}
-				parent = (<IShadowRoot>parent).host;
-			} else {
-				parent = parent.parentNode;
-			}
+		if (this._rootNode && !options?.composed) {
+			return this._rootNode;
 		}
 
-		return null;
+		return this.ownerDocument;
 	}
 
 	/**
@@ -234,8 +190,11 @@ export default class Node extends EventTarget implements INode {
 	public cloneNode(deep = false): INode {
 		const clone = new (<typeof Node>this.constructor)();
 
-		for (const node of clone.childNodes.slice()) {
-			node.parentNode.removeChild(node);
+		// Document has childNodes directly when it is created
+		if (clone.childNodes.length) {
+			for (const node of clone.childNodes.slice()) {
+				node.parentNode.removeChild(node);
+			}
 		}
 
 		if (deep) {
@@ -281,8 +240,7 @@ export default class Node extends EventTarget implements INode {
 
 		this.childNodes.push(node);
 
-		(<Node>node.parentNode) = this;
-		node.isConnected = this.isConnected;
+		(<Node>node)._connectToNode(this);
 
 		// MutationObserver
 		if (this._observers.length > 0) {
@@ -318,8 +276,7 @@ export default class Node extends EventTarget implements INode {
 
 		this.childNodes.splice(index, 1);
 
-		(<Node>node.parentNode) = null;
-		node.isConnected = false;
+		(<Node>node)._connectToNode(null);
 
 		// MutationObserver
 		if (this._observers.length > 0) {
@@ -384,8 +341,7 @@ export default class Node extends EventTarget implements INode {
 
 		this.childNodes.splice(index, 0, newNode);
 
-		(<Node>newNode.parentNode) = this;
-		newNode.isConnected = this.isConnected;
+		(<Node>newNode)._connectToNode(this);
 
 		// MutationObserver
 		if (this._observers.length > 0) {
@@ -432,8 +388,16 @@ export default class Node extends EventTarget implements INode {
 
 		const returnValue = super.dispatchEvent(event);
 
-		if (event.bubbles && this.parentNode !== null && !event._propagationStopped) {
-			return this.parentNode.dispatchEvent(event);
+		if (event.bubbles && !event._propagationStopped) {
+			if (this.parentNode) {
+				return this.parentNode.dispatchEvent(event);
+			}
+
+			// eslint-disable-next-line
+			if(event.composed && (<any>this).host) {
+				// eslint-disable-next-line
+				return (<any>this).host.dispatchEvent(event);
+			}
 		}
 
 		return returnValue;
@@ -480,4 +444,38 @@ export default class Node extends EventTarget implements INode {
 			}
 		}
 	}
+
+	/**
+	 * Connects this element to another element.
+	 *
+	 * @param parentNode Parent node.
+	 */
+	public _connectToNode(parentNode: INode = null): void {
+		const isConnected = !!parentNode && parentNode.isConnected;
+
+		if (this.nodeType !== Node.DOCUMENT_FRAGMENT_NODE) {
+			(<INode>this.parentNode) = parentNode;
+			(<Node>this)._rootNode = isConnected && parentNode ? (<Node>parentNode)._rootNode : null;
+		}
+
+		if (this.isConnected !== isConnected) {
+			(<boolean>this.isConnected) = isConnected;
+
+			if (isConnected && this.connectedCallback) {
+				this.connectedCallback();
+			} else if (!isConnected && this.disconnectedCallback) {
+				this.disconnectedCallback();
+			}
+
+			for (const child of this.childNodes) {
+				(<Node>child)._connectToNode(this);
+			}
+
+			// eslint-disable-next-line
+			if ((<any>this).shadowRoot) {
+				// eslint-disable-next-line
+				(<any>this).shadowRoot._connectToNode(this);
+			}
+		}
+	}
 }
diff --git a/packages/happy-dom/src/nodes/shadow-root/ShadowRoot.ts b/packages/happy-dom/src/nodes/shadow-root/ShadowRoot.ts
index 393e6656..f49c4b42 100644
--- a/packages/happy-dom/src/nodes/shadow-root/ShadowRoot.ts
+++ b/packages/happy-dom/src/nodes/shadow-root/ShadowRoot.ts
@@ -4,6 +4,7 @@ import XMLSerializer from '../../xml-serializer/XMLSerializer';
 import IElement from '../element/IElement';
 import CSSStyleSheet from '../../css/CSSStyleSheet';
 import IShadowRoot from './IShadowRoot';
+import IHTMLElement from '../../nodes/html-element/IHTMLElement';
 
 /**
  * ShadowRoot.
@@ -42,6 +43,19 @@ export default class ShadowRoot extends DocumentFragment implements IShadowRoot
 		}
 	}
 
+	/**
+	 * Returns active element.
+	 *
+	 * @returns Active element.
+	 */
+	public get activeElement(): IHTMLElement {
+		const activeElement: IHTMLElement = this.ownerDocument['_activeElement'];
+		if (activeElement && activeElement.getRootNode() === this) {
+			return activeElement;
+		}
+		return null;
+	}
+
 	/**
 	 * Converts to string.
 	 *
diff --git a/packages/happy-dom/src/query-selector/SelectorItem.ts b/packages/happy-dom/src/query-selector/SelectorItem.ts
index 62c9d9f5..b9a83b69 100644
--- a/packages/happy-dom/src/query-selector/SelectorItem.ts
+++ b/packages/happy-dom/src/query-selector/SelectorItem.ts
@@ -3,7 +3,7 @@ import Element from '../nodes/element/Element';
 
 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)\)|:([a-zA-Z-]+)/g;
+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-]+/;
 
@@ -29,7 +29,8 @@ export default class SelectorItem {
 	constructor(selector: string) {
 		this.isAll = selector === '*';
 		this.isID = !this.isAll ? selector.startsWith('#') : false;
-		this.isAttribute = !this.isAll && !this.isID && selector.includes('[');
+		this.isAttribute =
+			!this.isAll && !this.isID && selector.includes('[') && !selector.includes(':not(');
 		this.isPseudo = !this.isAll && !this.isID && selector.includes(':');
 		this.isClass = !this.isAll && !this.isID && new RegExp(CLASS_REGEXP, 'g').test(selector);
 		this.tagName = !this.isAll && !this.isID ? selector.match(TAG_NAME_REGEXP) : null;
@@ -84,24 +85,17 @@ export default class SelectorItem {
 			while ((match = regexp.exec(selector))) {
 				if (match[1] && !this.matchesNthChild(element, match[1], match[2])) {
 					return false;
-				} else if (match[3] && !this.matchesPsuedo(element, match[3])) {
+				} else if (match[3] && this.matchesAttribute(element, match[3])) {
+					return false;
+				} else if (match[4] && !this.matchesPsuedo(element, match[4])) {
 					return false;
 				}
 			}
 		}
 
 		// Attribute match
-		if (this.isAttribute) {
-			const regexp = new RegExp(ATTRIBUTE_REGEXP, 'g');
-
-			while ((match = regexp.exec(selector))) {
-				if (
-					(match[1] && !this.matchesAttributeName(element, match[1])) ||
-					(match[2] && !this.matchesAttributeNameAndValue(element, match[2], match[4], match[3]))
-				) {
-					return false;
-				}
-			}
+		if (this.isAttribute && !this.matchesAttribute(element, selector)) {
+			return false;
 		}
 
 		return true;
@@ -217,6 +211,31 @@ export default class SelectorItem {
 		return false;
 	}
 
+	/**
+	 * Matches attribute.
+	 *
+	 * @param element Element.
+	 * @param selector Selector.
+	 * @returns True if it is a match.
+	 */
+	private matchesAttribute(element: Element, selector: string): boolean {
+		const regexp = new RegExp(ATTRIBUTE_REGEXP, 'g');
+		let match;
+
+		while ((match = regexp.exec(selector))) {
+			const isPsuedo = match.index > 0 && selector[match.index] === '(';
+			if (
+				!isPsuedo &&
+				((match[1] && !this.matchesAttributeName(element, match[1])) ||
+					(match[2] && !this.matchesAttributeNameAndValue(element, match[2], match[4], match[3])))
+			) {
+				return false;
+			}
+		}
+
+		return true;
+	}
+
 	/**
 	 * Matches attribute name only.
 	 *
@@ -224,7 +243,7 @@ export default class SelectorItem {
 	 * @param attributeName Attribute name.
 	 * @returns True if it is a match.
 	 */
-	private matchesAttributeName(element, attributeName): boolean {
+	private matchesAttributeName(element: Element, attributeName: string): boolean {
 		if (ATTRIBUTE_NAME_REGEXP.test(attributeName)) {
 			throw new DOMException(`The selector "${this.selector}" is not valid.`);
 		}
@@ -243,10 +262,10 @@ export default class SelectorItem {
 	 * @returns True if it is a match.
 	 */
 	private matchesAttributeNameAndValue(
-		element,
-		attributeName,
-		attributeValue,
-		matchType = null
+		element: Element,
+		attributeName: string,
+		attributeValue: string,
+		matchType: string = null
 	): boolean {
 		const attribute = element._attributes[attributeName.toLowerCase()];
 		const value = attributeValue;
diff --git a/packages/happy-dom/src/window/Window.ts b/packages/happy-dom/src/window/Window.ts
index 48d8ad8d..144fb88c 100644
--- a/packages/happy-dom/src/window/Window.ts
+++ b/packages/happy-dom/src/window/Window.ts
@@ -42,6 +42,7 @@ import FileReader from '../file/FileReader';
 import History from '../history/History';
 import CSSStyleDeclaration from '../css/CSSStyleDeclaration';
 import MouseEvent from '../event/events/MouseEvent';
+import PointerEvent from '../event/events/PointerEvent';
 import FocusEvent from '../event/events/FocusEvent';
 import WheelEvent from '../event/events/WheelEvent';
 import DataTransfer from '../event/DataTransfer';
@@ -111,6 +112,7 @@ export default class Window extends EventTarget implements IWindow, NodeJS.Globa
 	public readonly AnimationEvent = AnimationEvent;
 	public readonly KeyboardEvent = KeyboardEvent;
 	public readonly MouseEvent = MouseEvent;
+	public readonly PointerEvent = PointerEvent;
 	public readonly FocusEvent = FocusEvent;
 	public readonly WheelEvent = WheelEvent;
 	public readonly InputEvent = InputEvent;


Test patch:
diff --git a/packages/happy-dom/test/nodes/document-fragment/DocumentFragment.test.ts b/packages/happy-dom/test/nodes/document-fragment/DocumentFragment.test.ts
index c7c893c7..a115ab36 100644
--- a/packages/happy-dom/test/nodes/document-fragment/DocumentFragment.test.ts
+++ b/packages/happy-dom/test/nodes/document-fragment/DocumentFragment.test.ts
@@ -279,8 +279,10 @@ describe('DocumentFragment', () => {
 
 			const clone = documentFragment.cloneNode(false);
 
-			expect(clone).toEqual({ ...documentFragment, childNodes: [], children: [] });
-			expect(documentFragment !== clone).toBe(true);
+			expect(clone.nodeType).toBe(Node.DOCUMENT_FRAGMENT_NODE);
+			expect(clone._rootNode).toBe(clone);
+			expect(clone.childNodes.length).toBe(0);
+			expect(clone.children.length).toBe(0);
 		});
 
 		it('Makes a deep clone of the document fragment.', () => {
@@ -294,7 +296,8 @@ describe('DocumentFragment', () => {
 
 			const clone = documentFragment.cloneNode(true);
 
-			expect(clone).toEqual(documentFragment);
+			expect(clone.nodeType).toBe(Node.DOCUMENT_FRAGMENT_NODE);
+			expect(clone._rootNode).toBe(clone);
 			expect(clone.childNodes.length).toBe(3);
 			expect(clone.children).toEqual(
 				clone.childNodes.filter(node => node.nodeType === Node.ELEMENT_NODE)
diff --git a/packages/happy-dom/test/nodes/document/Document.test.ts b/packages/happy-dom/test/nodes/document/Document.test.ts
index 9e390b0d..c88bddfe 100644
--- a/packages/happy-dom/test/nodes/document/Document.test.ts
+++ b/packages/happy-dom/test/nodes/document/Document.test.ts
@@ -19,7 +19,8 @@ import HTMLTemplateElement from '../../../src/nodes/html-template-element/HTMLTe
 import IHTMLCollection from '../../../src/nodes/element/IHTMLCollection';
 import IElement from '../../../src/nodes/element/IElement';
 import INodeList from '../../../src/nodes/node/INodeList';
-import { IHTMLLinkElement } from '../../../src';
+import IHTMLElement from '../../../src/nodes/html-element/IHTMLElement';
+import IHTMLLinkElement from '../../../src/nodes/html-link-element/IHTMLLinkElement';
 import IResponse from '../../../src/window/IResponse';
 import ResourceFetcher from '../../../src/fetch/ResourceFetcher';
 import IHTMLScriptElement from '../../../src/nodes/html-script-element/IHTMLScriptElement';
@@ -248,6 +249,30 @@ describe('Document', () => {
 		});
 	});
 
+	describe('get activeElement()', () => {
+		it('Returns the currently active element.', () => {
+			const div = <IHTMLElement>document.createElement('div');
+			const span = <IHTMLElement>document.createElement('span');
+
+			document.appendChild(div);
+			document.appendChild(span);
+
+			expect(document.activeElement === document.body).toBe(true);
+
+			div.focus();
+
+			expect(document.activeElement === div).toBe(true);
+
+			span.focus();
+
+			expect(document.activeElement === span).toBe(true);
+
+			span.blur();
+
+			expect(document.activeElement === document.body).toBe(true);
+		});
+	});
+
 	describe('append()', () => {
 		it('Inserts a set of Node objects or DOMString objects after the last child of the ParentNode. DOMString objects are inserted as equivalent Text nodes.', () => {
 			const node1 = document.createComment('test1');
diff --git a/packages/happy-dom/test/nodes/element/Element.test.ts b/packages/happy-dom/test/nodes/element/Element.test.ts
index 81b4c211..bfe9cdcf 100644
--- a/packages/happy-dom/test/nodes/element/Element.test.ts
+++ b/packages/happy-dom/test/nodes/element/Element.test.ts
@@ -609,6 +609,56 @@ describe('Element', () => {
 
 			expect(element.matches('.container.active')).toBe(true);
 		});
+
+		it('Checks if the element matches any selector in a string separated by comma.', () => {
+			const element = document.createElement('div');
+
+			element.className = 'container active';
+
+			expect(element.matches('.container, .active')).toBe(true);
+		});
+	});
+
+	describe('closest()', () => {
+		it('Finds the closest matching element when connected to DOM.', () => {
+			const div = document.createElement('div');
+			const span = document.createElement('span');
+			const article = document.createElement('article');
+			const b = document.createElement('b');
+
+			span.className = 'span';
+
+			article.appendChild(b);
+			span.appendChild(article);
+			div.appendChild(span);
+
+			document.body.appendChild(div);
+
+			expect(b.closest('div') === div).toBe(true);
+			expect(b.closest('div span') === span).toBe(true);
+			expect(b.closest('div .span') === span).toBe(true);
+			expect(b.closest('div .span b') === b).toBe(true);
+			expect(b.closest('div .span article b') === b).toBe(true);
+		});
+
+		it('Finds the closest matching element when not connected to DOM.', () => {
+			const div = document.createElement('div');
+			const span = document.createElement('span');
+			const article = document.createElement('article');
+			const b = document.createElement('b');
+
+			span.className = 'span';
+
+			article.appendChild(b);
+			span.appendChild(article);
+			div.appendChild(span);
+
+			expect(b.closest('div') === div).toBe(true);
+			expect(b.closest('div span') === span).toBe(true);
+			expect(b.closest('div .span') === span).toBe(true);
+			expect(b.closest('div .span b') === b).toBe(true);
+			expect(b.closest('div .span article b') === b).toBe(true);
+		});
 	});
 
 	describe('querySelectorAll()', () => {
diff --git a/packages/happy-dom/test/nodes/html-element/HTMLElement.test.ts b/packages/happy-dom/test/nodes/html-element/HTMLElement.test.ts
index e47bbedb..757a44f5 100644
--- a/packages/happy-dom/test/nodes/html-element/HTMLElement.test.ts
+++ b/packages/happy-dom/test/nodes/html-element/HTMLElement.test.ts
@@ -1,3 +1,4 @@
+import PointerEvent from '../../../src/event/events/PointerEvent';
 import Document from '../../../src/nodes/document/Document';
 import HTMLElement from '../../../src/nodes/html-element/HTMLElement';
 import Window from '../../../src/window/Window';
@@ -240,36 +241,165 @@ describe('HTMLElement', () => {
 		});
 	}
 
-	for (const eventType of ['click', 'blur', 'focus', 'focusin', 'focusout']) {
-		describe(`${eventType}()`, () => {
-			it(`Dispatches a "${eventType}" event.`, () => {
-				let methodName = eventType;
-				let triggeredEvent = null;
-
-				if (eventType === 'focusin') {
-					methodName = 'focus';
-				} else if (eventType === 'focusout') {
-					methodName = 'blur';
-				}
-
-				element.addEventListener(eventType, event => {
-					triggeredEvent = event;
-				});
-				element[methodName]();
-				expect(triggeredEvent).toEqual({
-					_immediatePropagationStopped: false,
-					_propagationStopped: false,
-					bubbles: true,
-					cancelable: false,
-					composed: true,
-					currentTarget: element,
-					defaultPrevented: false,
-					target: element,
-					type: eventType
-				});
+	describe('click()', () => {
+		it('Dispatches "click" event.', () => {
+			let triggeredEvent = null;
+
+			element.addEventListener('click', event => {
+				triggeredEvent = event;
 			});
+
+			element.click();
+
+			const event = new PointerEvent('click', {
+				bubbles: true,
+				composed: true
+			});
+			event.target = element;
+			event.currentTarget = element;
+
+			expect(triggeredEvent).toEqual(event);
 		});
-	}
+	});
+
+	describe('blur()', () => {
+		it('Dispatches "blur" and "focusout" event.', () => {
+			let triggeredBlurEvent = null;
+			let triggeredFocusOutEvent = null;
+
+			document.body.appendChild(element);
+
+			element.addEventListener('blur', event => {
+				triggeredBlurEvent = event;
+			});
+
+			element.addEventListener('focusout', event => {
+				triggeredFocusOutEvent = event;
+			});
+
+			element.focus();
+			element.blur();
+
+			expect(triggeredBlurEvent.type).toBe('blur');
+			expect(triggeredBlurEvent.bubbles).toBe(true);
+			expect(triggeredBlurEvent.composed).toBe(true);
+			expect(triggeredBlurEvent.target === element).toBe(true);
+
+			expect(triggeredFocusOutEvent.type).toBe('focusout');
+			expect(triggeredFocusOutEvent.bubbles).toBe(true);
+			expect(triggeredFocusOutEvent.composed).toBe(true);
+			expect(triggeredFocusOutEvent.target === element).toBe(true);
+
+			expect(document.activeElement).toEqual(document.body);
+		});
+
+		it('Does not dispatch "blur" event if not connected to the DOM.', () => {
+			let triggeredEvent = null;
+
+			element.addEventListener('blur', event => {
+				triggeredEvent = event;
+			});
+
+			element.focus();
+			element.blur();
+
+			expect(triggeredEvent).toBe(null);
+		});
+
+		it('Does not dispatch "blur" event if it is not in focus.', () => {
+			let triggeredEvent = null;
+
+			document.body.appendChild(element);
+
+			element.addEventListener('blur', event => {
+				triggeredEvent = event;
+			});
+
+			element.blur();
+
+			expect(triggeredEvent).toBe(null);
+		});
+	});
+
+	describe('focus()', () => {
+		it('Dispatches "focus" and "focusin" event.', () => {
+			let triggeredFocusEvent = null;
+			let triggeredFocusInEvent = null;
+
+			document.body.appendChild(element);
+
+			element.addEventListener('focus', event => {
+				triggeredFocusEvent = event;
+			});
+
+			element.addEventListener('focusin', event => {
+				triggeredFocusInEvent = event;
+			});
+
+			element.focus();
+
+			expect(triggeredFocusEvent.type).toBe('focus');
+			expect(triggeredFocusEvent.bubbles).toBe(true);
+			expect(triggeredFocusEvent.composed).toBe(true);
+			expect(triggeredFocusEvent.target === element).toBe(true);
+
+			expect(triggeredFocusInEvent.type).toBe('focusin');
+			expect(triggeredFocusInEvent.bubbles).toBe(true);
+			expect(triggeredFocusInEvent.composed).toBe(true);
+			expect(triggeredFocusInEvent.target === element).toBe(true);
+
+			expect(document.activeElement).toEqual(element);
+		});
+
+		it('Does not dispatch "focus" event if not connected to the DOM.', () => {
+			let triggeredEvent = null;
+
+			element.addEventListener('focus', event => {
+				triggeredEvent = event;
+			});
+
+			element.focus();
+
+			expect(triggeredEvent).toBe(null);
+		});
+
+		it('Does not dispatch "focus" event if it is already focused.', () => {
+			let triggeredEvent = null;
+
+			document.body.appendChild(element);
+
+			element.focus();
+
+			element.addEventListener('focus', event => {
+				triggeredEvent = event;
+			});
+
+			element.focus();
+
+			expect(triggeredEvent).toBe(null);
+		});
+
+		it('Dispatches "blur" event on the previously focused element.', () => {
+			const previousElement = <HTMLElement>document.createElement('div');
+			let triggeredEvent = null;
+
+			document.body.appendChild(element);
+			document.body.appendChild(previousElement);
+
+			previousElement.focus();
+
+			previousElement.addEventListener('blur', event => {
+				triggeredEvent = event;
+			});
+
+			element.focus();
+
+			expect(triggeredEvent.type).toBe('blur');
+			expect(triggeredEvent.bubbles).toBe(true);
+			expect(triggeredEvent.composed).toBe(true);
+			expect(triggeredEvent.target === previousElement).toBe(true);
+		});
+	});
 
 	describe('setAttributeNode()', () => {
 		it('Sets css text of existing CSSStyleDeclaration.', () => {
diff --git a/packages/happy-dom/test/nodes/node/Node.test.ts b/packages/happy-dom/test/nodes/node/Node.test.ts
index 5ca6fd50..4c741641 100644
--- a/packages/happy-dom/test/nodes/node/Node.test.ts
+++ b/packages/happy-dom/test/nodes/node/Node.test.ts
@@ -94,23 +94,6 @@ describe('Node', () => {
 		});
 	});
 
-	describe('set isConnected()', () => {
-		it('Sets an element and all its children to be connected.', () => {
-			const div = document.createElement('div');
-			const span = document.createElement('span');
-			const text = document.createTextNode('text');
-
-			div.appendChild(span);
-			span.appendChild(text);
-
-			div.isConnected = true;
-
-			expect(div.isConnected).toBe(true);
-			expect(span.isConnected).toBe(true);
-			expect(text.isConnected).toBe(true);
-		});
-	});
-
 	describe('get nodeValue()', () => {
 		it('Returns null.', () => {
 			expect(new Node().nodeValue).toBe(null);
@@ -261,7 +244,7 @@ describe('Node', () => {
 
 			const rootNode = customElement.shadowRoot.querySelector('span').getRootNode();
 
-			expect(rootNode).toBe(customElement.shadowRoot);
+			expect(rootNode === customElement.shadowRoot).toBe(true);
 		});
 
 		it('Returns Document when used on a node inside a ShadowRoot and the option "composed" is set to "true".', () => {
@@ -273,7 +256,7 @@ describe('Node', () => {
 				.querySelector('span')
 				.getRootNode({ composed: true });
 
-			expect(rootNode).toBe(document);
+			expect(rootNode === document).toBe(true);
 		});
 
 		it('Returns Document when the node is not inside a ShadowRoot.', () => {
@@ -285,7 +268,7 @@ describe('Node', () => {
 
 			const rootNode = spanElement.getRootNode();
 
-			expect(rootNode).toBe(document);
+			expect(rootNode === document).toBe(true);
 		});
 	});
 
diff --git a/packages/happy-dom/test/nodes/shadow-root/ShadowRoot.test.ts b/packages/happy-dom/test/nodes/shadow-root/ShadowRoot.test.ts
index c756eaaf..3c04476d 100644
--- a/packages/happy-dom/test/nodes/shadow-root/ShadowRoot.test.ts
+++ b/packages/happy-dom/test/nodes/shadow-root/ShadowRoot.test.ts
@@ -1,3 +1,4 @@
+import IHTMLElement from '../../../src/nodes/html-element/IHTMLElement';
 import Window from '../../../src/window/Window';
 import CustomElement from '../../CustomElement';
 
@@ -35,6 +36,40 @@ describe('ShadowRoot', () => {
 		});
 	});
 
+	describe('get activeElement()', () => {
+		it('Returns the currently active element within the ShadowRoot.', () => {
+			const customElement = document.createElement('custom-element');
+			const shadowRoot = customElement.shadowRoot;
+			const div = <IHTMLElement>document.createElement('div');
+			const span = <IHTMLElement>document.createElement('span');
+
+			document.body.appendChild(customElement);
+
+			shadowRoot.appendChild(div);
+			shadowRoot.appendChild(span);
+
+			expect(shadowRoot.activeElement === null).toBe(true);
+
+			div.focus();
+
+			expect(shadowRoot.activeElement === div).toBe(true);
+
+			span.focus();
+
+			expect(shadowRoot.activeElement === span).toBe(true);
+
+			span.blur();
+
+			expect(shadowRoot.activeElement === null).toBe(true);
+
+			document.body.appendChild(span);
+
+			span.focus();
+
+			expect(shadowRoot.activeElement === null).toBe(true);
+		});
+	});
+
 	describe('toString()', () => {
 		it('Returns the innerHTML of the shadow root.', () => {
 			const html = '<div attr1="value1" attr2="value2"><span>Test</span></div>';
diff --git a/packages/happy-dom/test/query-selector/QuerySelector.test.ts b/packages/happy-dom/test/query-selector/QuerySelector.test.ts
index 0f40b0a6..65b01ba2 100644
--- a/packages/happy-dom/test/query-selector/QuerySelector.test.ts
+++ b/packages/happy-dom/test/query-selector/QuerySelector.test.ts
@@ -350,6 +350,15 @@ describe('QuerySelector', () => {
 			expect(elements[4]).toBe(container.children[1].children[0]);
 		});
 
+		it('Returns all elements matching "span:not([type=hidden])".', () => {
+			const container = document.createElement('div');
+			container.innerHTML = QuerySelectorHTML;
+			const elements = container.querySelectorAll('span:not([type=hidden])');
+
+			expect(elements.length).toBe(1);
+			expect(elements[0]).toBe(container.children[0].children[1].children[1]);
+		});
+
 		it('Returns all span elements matching span:nth-child(1) or span:nth-child(2).', () => {
 			const container = document.createElement('div');
 			container.innerHTML = QuerySelectorHTML;
diff --git a/packages/happy-dom/test/query-selector/data/QuerySelectorHTML.ts b/packages/happy-dom/test/query-selector/data/QuerySelectorHTML.ts
index 947c91e1..8baa25de 100644
--- a/packages/happy-dom/test/query-selector/data/QuerySelectorHTML.ts
+++ b/packages/happy-dom/test/query-selector/data/QuerySelectorHTML.ts
@@ -4,7 +4,7 @@ export default `
 		<h1>Heading1</h1>
 		<!-- Comment 2 !-->
 		<div class="class1 class2">
-			<span class="class1 class2" attr1="value1" attr2="word1 word2" attr3="bracket[]bracket">Span1</span>
+			<span class="class1 class2" attr1="value1" attr2="word1 word2" attr3="bracket[]bracket" type="hidden">Span1</span>
 			<span class="class1 class2" attr1="value1">Span2</span>
 		</div>
 	</div>