Repository: capricorn86/happy-dom
Pull request #385 (2022-03-01 17:20:12)

#384@minor: Adds support for HTMLLinkElement.refList and using indice…
…s with Element.classList.


Patch:
diff --git a/packages/happy-dom/src/css/CSSStyleDeclaration.ts b/packages/happy-dom/src/css/CSSStyleDeclaration.ts
index 6594e552..5a932aa9 100644
--- a/packages/happy-dom/src/css/CSSStyleDeclaration.ts
+++ b/packages/happy-dom/src/css/CSSStyleDeclaration.ts
@@ -4898,7 +4898,7 @@ export default class CSSStyleDeclaration {
 	 * @returns Item.
 	 */
 	public item(index: number): string {
-		return this[String(index)] || '';
+		return this[index] || '';
 	}
 
 	/**
diff --git a/packages/happy-dom/src/dom-token-list/DOMTokenList.ts b/packages/happy-dom/src/dom-token-list/DOMTokenList.ts
new file mode 100644
index 00000000..2303853a
--- /dev/null
+++ b/packages/happy-dom/src/dom-token-list/DOMTokenList.ts
@@ -0,0 +1,219 @@
+import DOMException from '../exception/DOMException';
+import Element from '../nodes/element/Element';
+import IDOMTokenList from './IDOMTokenList';
+
+/**
+ * DOM Token List.
+ *
+ * Reference:
+ * https://developer.mozilla.org/en-US/docs/Web/API/DOMTokenList.
+ */
+export default class DOMTokenList implements IDOMTokenList {
+	public readonly length = 0;
+	private _ownerElement: Element;
+	private _attributeName: string;
+
+	/**
+	 * Constructor.
+	 *
+	 * @param ownerElement Owner element.
+	 * @param attributeName Attribute name.
+	 */
+	constructor(ownerElement: Element, attributeName) {
+		this._ownerElement = ownerElement;
+		this._attributeName = attributeName;
+		this._updateIndices();
+	}
+
+	/**
+	 * Set value.
+	 *
+	 * @param value Value.
+	 */
+	public set value(value: string) {
+		this._ownerElement.setAttributeNS(null, this._attributeName, value);
+	}
+
+	/**
+	 * Get value.
+	 */
+	public get value(): string {
+		return this._ownerElement.getAttributeNS(null, this._attributeName);
+	}
+
+	/**
+	 * Get ClassName.
+	 *
+	 * @param index Index.
+	 * */
+	public item(index: number | string): string {
+		index = typeof index === 'number' ? index : 0;
+		return index >= 0 && this[index] ? this[index] : null;
+	}
+
+	/**
+	 * Replace Token.
+	 *
+	 * @param token Token.
+	 * @param newToken NewToken.
+	 */
+	public replace(token: string, newToken: string): boolean {
+		const attr = this._ownerElement.getAttributeNS(null, this._attributeName);
+		const list = attr ? attr.split(' ') : [];
+		const index = list.indexOf(token);
+		if (index === -1) {
+			return false;
+		}
+		list[index] = newToken;
+		this._ownerElement.setAttributeNS(null, this._attributeName, list.join(' '));
+		return true;
+	}
+
+	/**
+	 * Supports.
+	 *
+	 * @param token Token.
+	 */
+	public supports(token: string): boolean {
+		// TODO: Only implemented for classList, which does not have any supported tokens
+		throw new DOMException(
+			`Failed to execute '${token}' on 'DOMTokenList': DOMTokenList has no supported tokens.`,
+			'TypeError'
+		);
+	}
+
+	/**
+	 * Returns an iterator, allowing you to go through all values of the key/value pairs contained in this object.
+	 *
+	 *
+	 */
+	public values(): IterableIterator<string> {
+		const attr = this._ownerElement.getAttributeNS(null, this._attributeName);
+		return (attr ? attr.split(' ') : []).values();
+	}
+
+	/**
+	 * Returns an iterator, allowing you to go through all key/value pairs contained in this object.
+	 *
+	 *
+	 */
+	public entries(): IterableIterator<[number, string]> {
+		const attr = this._ownerElement.getAttributeNS(null, this._attributeName);
+		return (attr ? attr.split(' ') : []).entries();
+	}
+
+	/**
+	 * Executes a provided callback function once for each DOMTokenList element.
+	 *
+	 * @param callback
+	 * @param thisArg
+	 */
+	public forEach(callback: (currentValue, currentIndex, listObj) => void, thisArg?: this): void {
+		const attr = this._ownerElement.getAttributeNS(null, this._attributeName);
+		return (attr ? attr.split(' ') : []).forEach(callback, thisArg);
+	}
+
+	/**
+	 * Returns an iterator, allowing you to go through all keys of the key/value pairs contained in this object.
+	 *
+	 */
+	public keys(): IterableIterator<number> {
+		const attr = this._ownerElement.getAttributeNS(null, this._attributeName);
+		return (attr ? attr.split(' ') : []).keys();
+	}
+
+	/**
+	 * Adds tokens.
+	 *
+	 * @param tokens Tokens.
+	 */
+	public add(...tokens: string[]): void {
+		const attr = this._ownerElement.getAttributeNS(null, this._attributeName);
+		const list = attr ? attr.split(' ') : [];
+
+		for (const token of tokens) {
+			const index = list.indexOf(token);
+			if (index === -1) {
+				list.push(token);
+			} else {
+				list[index] = token;
+			}
+		}
+
+		this._ownerElement.setAttributeNS(null, this._attributeName, list.join(' '));
+	}
+
+	/**
+	 * Removes tokens.
+	 *
+	 * @param tokens Tokens.
+	 */
+	public remove(...tokens: string[]): void {
+		const attr = this._ownerElement.getAttributeNS(null, this._attributeName);
+		const list = attr ? attr.split(' ') : [];
+
+		for (const token of tokens) {
+			const index = list.indexOf(token);
+			if (index !== -1) {
+				list.splice(index, 1);
+			}
+		}
+
+		this._ownerElement.setAttributeNS(null, this._attributeName, list.join(' '));
+	}
+
+	/**
+	 * Check if the list contains a class.
+	 *
+	 * @param className Class name.
+	 * @returns TRUE if it contains.
+	 */
+	public contains(className: string): boolean {
+		const attr = this._ownerElement.getAttributeNS(null, this._attributeName);
+		return (attr ? attr.split(' ') : []).includes(className);
+	}
+
+	/**
+	 * Toggle a class name.
+	 *
+	 * @param token A string representing the class name you want to toggle.
+	 * @param [force] If included, turns the toggle into a one way-only operation. If set to `false`, then class name will only be removed, but not added. If set to `true`, then class name will only be added, but not removed.
+	 * @returns A boolean value, `true` or `false`, indicating whether class name is in the list after the call or not.
+	 */
+	public toggle(token: string, force?: boolean): boolean {
+		let shouldAdd: boolean;
+
+		if (force !== undefined) {
+			shouldAdd = force;
+		} else {
+			shouldAdd = !this.contains(token);
+		}
+
+		if (shouldAdd) {
+			this.add(token);
+			return true;
+		}
+
+		this.remove(token);
+
+		return false;
+	}
+
+	/**
+	 * Updates indices.
+	 */
+	public _updateIndices(): void {
+		const attr = this._ownerElement.getAttribute('class');
+		const list = attr ? Array.from(new Set(attr.split(' '))) : [];
+
+		for (let i = list.length - 1, max = this.length; i < max; i++) {
+			delete this[i];
+		}
+
+		for (let i = 0, max = list.length; i < max; i++) {
+			this[i] = list[i];
+		}
+
+		(<number>this.length) = list.length;
+	}
+}
diff --git a/packages/happy-dom/src/nodes/element/IDOMTokenList.ts b/packages/happy-dom/src/dom-token-list/IDOMTokenList.ts
similarity index 65%
rename from packages/happy-dom/src/nodes/element/IDOMTokenList.ts
rename to packages/happy-dom/src/dom-token-list/IDOMTokenList.ts
index 502fbe11..6a7c46bc 100644
--- a/packages/happy-dom/src/nodes/element/IDOMTokenList.ts
+++ b/packages/happy-dom/src/dom-token-list/IDOMTokenList.ts
@@ -1,8 +1,10 @@
 /**
  * IDOMTokenList.
  */
-export default interface IDOMTokenList<T> {
-	item(index: number): string;
+export default interface IDOMTokenList {
+	value: string;
+	readonly length: number;
+	item(index: number | string): string;
 	contains(token: string): boolean;
 	add(...tokens: string[]): void;
 	remove(...tokens: string[]): void;
@@ -10,8 +12,8 @@ export default interface IDOMTokenList<T> {
 	replace(token: string, newToken: string): boolean;
 	supports(token: string): boolean;
 
-	values(): IterableIterator<T>;
-	entries(): IterableIterator<[number, T]>;
+	values(): IterableIterator<string>;
+	entries(): IterableIterator<[number, string]>;
 	forEach(callback: (currentValue, currentIndex, listObj) => void, thisArg?: this): void;
 	keys(): IterableIterator<number>;
 }
diff --git a/packages/happy-dom/src/nodes/document/Document.ts b/packages/happy-dom/src/nodes/document/Document.ts
index b69dbe93..676b44f6 100644
--- a/packages/happy-dom/src/nodes/document/Document.ts
+++ b/packages/happy-dom/src/nodes/document/Document.ts
@@ -21,6 +21,7 @@ import CSSStyleSheet from '../../css/CSSStyleSheet';
 import DOMException from '../../exception/DOMException';
 import CookieUtility from '../../cookie/CookieUtility';
 import IElement from '../element/IElement';
+import IHTMLScriptElement from '../html-script-element/IHTMLScriptElement';
 import IHTMLElement from '../html-element/IHTMLElement';
 import IDocumentType from '../document-type/IDocumentType';
 import INode from '../node/INode';
@@ -245,8 +246,8 @@ export default class Document extends Node implements IDocument {
 	 *
 	 * @returns Scripts.
 	 */
-	public get scripts(): IHTMLCollection<IElement> {
-		return this.getElementsByTagName('script');
+	public get scripts(): IHTMLCollection<IHTMLScriptElement> {
+		return <IHTMLCollection<IHTMLScriptElement>>this.getElementsByTagName('script');
 	}
 
 	/**
diff --git a/packages/happy-dom/src/nodes/document/IDocument.ts b/packages/happy-dom/src/nodes/document/IDocument.ts
index 8c8b3098..08f4facb 100644
--- a/packages/happy-dom/src/nodes/document/IDocument.ts
+++ b/packages/happy-dom/src/nodes/document/IDocument.ts
@@ -12,6 +12,11 @@ import INode from '../node/INode';
 import ICharacterData from '../character-data/ICharacterData';
 import IDocumentFragment from '../document-fragment/IDocumentFragment';
 import Selection from '../../selection/Selection';
+import IHTMLCollection from '../element/IHTMLCollection';
+import IHTMLScriptElement from '../html-script-element/IHTMLScriptElement';
+import CSSStyleSheet from '../../css/CSSStyleSheet';
+import Location from '../../location/Location';
+import DocumentReadyStateEnum from './DocumentReadyStateEnum';
 
 /**
  * Document.
@@ -24,7 +29,13 @@ export default interface IDocument extends IParentNode {
 	readonly doctype: IDocumentType;
 	readonly body: IHTMLElement;
 	readonly head: IHTMLElement;
+	readonly scripts: IHTMLCollection<IHTMLScriptElement>;
 	readonly activeElement: IHTMLElement;
+	readonly styleSheets: CSSStyleSheet[];
+	readonly scrollingElement: IHTMLElement;
+	readonly location: Location;
+	readonly readyState: DocumentReadyStateEnum;
+	cookie: string;
 
 	/**
 	 * Replaces the document HTML with new HTML.
@@ -69,7 +80,7 @@ export default interface IDocument extends IParentNode {
 	 * @param  data Text data.
 	 * @returns Text node.
 	 */
-	createTextNode(data: string): ICharacterData;
+	createTextNode(data?: string): ICharacterData;
 
 	/**
 	 * Creates a comment node.
@@ -77,7 +88,7 @@ export default interface IDocument extends IParentNode {
 	 * @param  data Text data.
 	 * @returns Text node.
 	 */
-	createComment(data: string): ICharacterData;
+	createComment(data?: string): ICharacterData;
 
 	/**
 	 * Creates a document fragment.
diff --git a/packages/happy-dom/src/nodes/element/DOMTokenList.ts b/packages/happy-dom/src/nodes/element/DOMTokenList.ts
deleted file mode 100644
index 90a37de0..00000000
--- a/packages/happy-dom/src/nodes/element/DOMTokenList.ts
+++ /dev/null
@@ -1,201 +0,0 @@
-import DOMException from '../../exception/DOMException';
-import Element from './Element';
-import IDOMTokenList from './IDOMTokenList';
-
-/**
- * DOM Token List.
- */
-export default class DOMTokenList implements IDOMTokenList<string> {
-	private _ownerElement: Element;
-	private _list: string[];
-	/**
-	 * Adds class names.
-	 *
-	 * @param ownerElement Owner element.
-	 */
-	constructor(ownerElement: Element) {
-		this._ownerElement = ownerElement;
-	}
-
-	/**
-	 * Get ClassName.
-	 *
-	 * @param index Index.
-	 * */
-	public item(index: number): string {
-		this._refreshListFromClass();
-		return index >= 0 && this._list[index] ? this._list[index] : null;
-	}
-
-	/**
-	 * Replace Token.
-	 *
-	 * @param token Token.
-	 * @param newToken NewToken.
-	 */
-	public replace(token: string, newToken: string): boolean {
-		if (!this.contains(token)) {
-			return false;
-		}
-		const index = this._list.indexOf(token);
-		this._list[index] = newToken;
-		const value = this._list ? this._list.join(' ') : '';
-		this._ownerElement.setAttribute('class', value);
-		return true;
-	}
-
-	/**
-	 * Supports.
-	 *
-	 * @param token Token.
-	 */
-	public supports(token: string): boolean {
-		// TODO May IT IN ERROR.
-		throw new DOMException(
-			`Failed to execute '${token}' on 'DOMTokenList': DOMTokenList has no supported tokens.`,
-			'TypeError'
-		);
-	}
-	/**
-	 * Returns an iterator, allowing you to go through all values of the key/value pairs contained in this object.
-	 *
-	 *
-	 */
-	public values(): IterableIterator<string> {
-		this._refreshListFromClass();
-		return this._list.values();
-	}
-	/**
-	 * Returns an iterator, allowing you to go through all key/value pairs contained in this object.
-	 *
-	 *
-	 */
-	public entries(): IterableIterator<[number, string]> {
-		this._refreshListFromClass();
-		return this._list.entries();
-	}
-	/**
-	 * Executes a provided callback function once for each DOMTokenList element.
-	 *
-	 * @param callback
-	 * @param thisArg
-	 */
-	public forEach(callback: (currentValue, currentIndex, listObj) => void, thisArg?: this): void {
-		this._refreshListFromClass();
-		return this._list.forEach(callback, thisArg);
-	}
-	/**
-	 * Returns an iterator, allowing you to go through all keys of the key/value pairs contained in this object.
-	 *
-	 */
-	public keys(): IterableIterator<number> {
-		this._refreshListFromClass();
-		return this._list.keys();
-	}
-
-	/**
-	 * Adds class names.
-	 *
-	 * @param classNames Class names.
-	 */
-	public add(...classNames: string[]): void {
-		this._refreshListFromClass();
-		for (const className of classNames) {
-			if (!this._list.includes(className)) {
-				if (className.includes(' ')) {
-					throw new DOMException(
-						`Failed to execute 'add' on 'DOMTokenList': The token provided ('${className}') contains HTML space characters, which are not valid in tokens.`
-					);
-				}
-				this._list.push(className);
-			}
-		}
-		this._list = Array.from(new Set(this._list));
-		const value = this._list ? this._list.join(' ') : '';
-		this._ownerElement.setAttribute('class', value);
-	}
-
-	/**
-	 * Removes class names.
-	 *
-	 * @param classNames Class names.
-	 */
-	public remove(...classNames: string[]): void {
-		this._refreshListFromClass();
-		for (const className of classNames) {
-			const index = this._list.indexOf(className);
-			if (index !== -1) {
-				this._list.splice(index, 1);
-			}
-		}
-		const value = this._list ? this._list.join(' ') : '';
-		this._ownerElement.setAttribute('class', value);
-	}
-
-	/**
-	 * Check if the list contains a class.
-	 *
-	 * @param className Class name.
-	 * @returns TRUE if it contains.
-	 */
-	public contains(className: string): boolean {
-		this._refreshListFromClass();
-		return this._list.includes(className);
-	}
-
-	/**
-	 * Toggle a class name.
-	 *
-	 * @param className A string representing the class name you want to toggle.
-	 * @param force If included, turns the toggle into a one way-only operation. If set to `false`, then class name will only be removed, but not added. If set to `true`, then class name will only be added, but not removed.
-	 * @returns A boolean value, `true` or `false`, indicating whether class name is in the list after the call or not.
-	 */
-	public toggle(className: string, force?: boolean): boolean {
-		let shouldAdd: boolean;
-		if (force !== undefined) {
-			shouldAdd = force;
-		} else {
-			shouldAdd = !this.contains(className);
-		}
-
-		if (shouldAdd) {
-			this.add(className);
-			return true;
-		}
-
-		this.remove(className);
-		return false;
-	}
-
-	/**
-	 * Refresh list from class.
-	 */
-	private _refreshListFromClass(): void {
-		const attr = this._ownerElement.getAttribute('class');
-		this._list = attr ? Array.from(new Set(attr.split(' '))) : [];
-	}
-
-	/**
-	 * Set Value.
-	 */
-	public set value(value: string) {
-		this._ownerElement.setAttribute('class', value);
-		this._list = value ? Array.from(new Set(value.split(' '))) : [];
-	}
-
-	/**
-	 * Get Value.
-	 */
-	public get value(): string {
-		this._refreshListFromClass();
-		return this._list ? this._list.join(' ') : '';
-	}
-
-	/**
-	 * Get Length.
-	 */
-	public get length(): number {
-		this._refreshListFromClass();
-		return this._list.length;
-	}
-}
diff --git a/packages/happy-dom/src/nodes/element/Element.ts b/packages/happy-dom/src/nodes/element/Element.ts
index fe1f00d7..d2406b0e 100644
--- a/packages/happy-dom/src/nodes/element/Element.ts
+++ b/packages/happy-dom/src/nodes/element/Element.ts
@@ -3,7 +3,8 @@ import ShadowRoot from '../shadow-root/ShadowRoot';
 import Attr from '../../attribute/Attr';
 import DOMRect from './DOMRect';
 import Range from './Range';
-import DOMTokenList from './DOMTokenList';
+import DOMTokenList from '../../dom-token-list/DOMTokenList';
+import IDOMTokenList from '../../dom-token-list/IDOMTokenList';
 import QuerySelector from '../../query-selector/QuerySelector';
 import SelectorItem from '../../query-selector/SelectorItem';
 import MutationRecord from '../../mutation-observer/MutationRecord';
@@ -37,13 +38,25 @@ export default class Element extends Node implements IElement {
 	public nodeType = Node.ELEMENT_NODE;
 	public shadowRoot: IShadowRoot = null;
 	public _attributes: { [k: string]: Attr } = {};
-	public readonly classList = new DOMTokenList(this);
 	public scrollTop = 0;
 	public scrollLeft = 0;
 	public children: IHTMLCollection<IElement> = HTMLCollectionFactory.create();
-
 	public readonly namespaceURI: string = null;
 
+	private _classList: DOMTokenList = null;
+
+	/**
+	 * Returns class list.
+	 *
+	 * @returns Class list.
+	 */
+	public get classList(): IDOMTokenList {
+		if (!this._classList) {
+			this._classList = new DOMTokenList(this, 'class');
+		}
+		return <IDOMTokenList>this._classList;
+	}
+
 	/**
 	 * Returns ID.
 	 *
@@ -770,6 +783,8 @@ export default class Element extends Node implements IElement {
 
 		this._attributes[name] = attribute;
 
+		this._updateDomListIndices();
+
 		if (
 			this.attributeChangedCallback &&
 			(<typeof Element>this.constructor)._observedAttributes &&
@@ -843,6 +858,8 @@ export default class Element extends Node implements IElement {
 	public removeAttributeNode(attribute: Attr): void {
 		delete this._attributes[attribute.name];
 
+		this._updateDomListIndices();
+
 		if (
 			this.attributeChangedCallback &&
 			(<typeof Element>this.constructor)._observedAttributes &&
@@ -934,4 +951,13 @@ export default class Element extends Node implements IElement {
 		}
 		return name.toLowerCase();
 	}
+
+	/**
+	 * Updates DOM list indices.
+	 */
+	protected _updateDomListIndices(): void {
+		if (this._classList) {
+			this._classList._updateIndices();
+		}
+	}
 }
diff --git a/packages/happy-dom/src/nodes/element/IElement.ts b/packages/happy-dom/src/nodes/element/IElement.ts
index 0412cfc5..e0e3f3b4 100644
--- a/packages/happy-dom/src/nodes/element/IElement.ts
+++ b/packages/happy-dom/src/nodes/element/IElement.ts
@@ -2,7 +2,7 @@ import IShadowRoot from '../shadow-root/IShadowRoot';
 import Attr from '../../attribute/Attr';
 import DOMRect from './DOMRect';
 import Range from './Range';
-import DOMTokenList from './DOMTokenList';
+import IDOMTokenList from '../../dom-token-list/IDOMTokenList';
 import INode from './../node/INode';
 import IChildNode from '../child-node/IChildNode';
 import IParentNode from '../parent-node/IParentNode';
@@ -16,7 +16,7 @@ export type TInsertAdjacentPositions = 'beforebegin' | 'afterbegin' | 'beforeend
 export default interface IElement extends IChildNode, INonDocumentTypeChildNode, IParentNode {
 	readonly tagName: string;
 	readonly shadowRoot: IShadowRoot;
-	readonly classList: DOMTokenList;
+	readonly classList: IDOMTokenList;
 	readonly namespaceURI: string;
 	scrollTop: number;
 	scrollLeft: number;
diff --git a/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElement.ts b/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElement.ts
index a4a62fc4..a031e656 100644
--- a/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElement.ts
+++ b/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElement.ts
@@ -10,8 +10,6 @@ import IHTMLLabelElement from './IHTMLLabelElement';
  * https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement.
  */
 export default class HTMLLabelElement extends HTMLElement implements IHTMLLabelElement {
-	public _htmlFor: string = null;
-
 	/**
 	 * Returns a string containing the ID of the labeled control. This reflects the "for" attribute.
 	 *
@@ -73,8 +71,6 @@ export default class HTMLLabelElement extends HTMLElement implements IHTMLLabelE
 	 * @returns Cloned node.
 	 */
 	public cloneNode(deep = false): IHTMLLabelElement {
-		const clone = <HTMLLabelElement>super.cloneNode(deep);
-		clone._htmlFor = this._htmlFor;
-		return clone;
+		return <HTMLLabelElement>super.cloneNode(deep);
 	}
 }
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 9c135474..fdcfc35f 100644
--- a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts
+++ b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts
@@ -7,6 +7,8 @@ import IHTMLLinkElement from './IHTMLLinkElement';
 import Event from '../../event/Event';
 import ErrorEvent from '../../event/events/ErrorEvent';
 import INode from '../../nodes/node/INode';
+import DOMTokenList from '../../dom-token-list/DOMTokenList';
+import IDOMTokenList from '../../dom-token-list/IDOMTokenList';
 
 /**
  * HTML Link Element.
@@ -19,6 +21,19 @@ export default class HTMLLinkElement extends HTMLElement implements IHTMLLinkEle
 	public onload: (event: Event) => void = null;
 	public readonly sheet: CSSStyleSheet = null;
 	public _evaluateCSS = true;
+	private _relList: DOMTokenList = null;
+
+	/**
+	 * Returns rel list.
+	 *
+	 * @returns Rel list.
+	 */
+	public get relList(): IDOMTokenList {
+		if (!this._relList) {
+			this._relList = new DOMTokenList(this, 'rel');
+		}
+		return <IDOMTokenList>this._relList;
+	}
 
 	/**
 	 * Returns as.
@@ -259,4 +274,15 @@ export default class HTMLLinkElement extends HTMLElement implements IHTMLLinkEle
 			}
 		}
 	}
+
+	/**
+	 * Updates DOM list indices.
+	 */
+	protected _updateDomListIndices(): void {
+		super._updateDomListIndices();
+
+		if (this._relList) {
+			this._relList._updateIndices();
+		}
+	}
 }
diff --git a/packages/happy-dom/src/nodes/html-link-element/IHTMLLinkElement.ts b/packages/happy-dom/src/nodes/html-link-element/IHTMLLinkElement.ts
index 737440a9..7ef356f7 100644
--- a/packages/happy-dom/src/nodes/html-link-element/IHTMLLinkElement.ts
+++ b/packages/happy-dom/src/nodes/html-link-element/IHTMLLinkElement.ts
@@ -1,4 +1,5 @@
 import CSSStyleSheet from '../../css/CSSStyleSheet';
+import IDOMTokenList from '../../dom-token-list/IDOMTokenList';
 import IHTMLElement from '../html-element/IHTMLElement';
 
 /**
@@ -9,6 +10,7 @@ import IHTMLElement from '../html-element/IHTMLElement';
  */
 export default interface IHTMLLinkElement extends IHTMLElement {
 	readonly sheet: CSSStyleSheet;
+	readonly relList: IDOMTokenList;
 	as: string;
 	crossOrigin: string;
 	href: string;
diff --git a/packages/happy-dom/src/window/IWindow.ts b/packages/happy-dom/src/window/IWindow.ts
index 92b14d1c..3f4a0b28 100644
--- a/packages/happy-dom/src/window/IWindow.ts
+++ b/packages/happy-dom/src/window/IWindow.ts
@@ -69,11 +69,12 @@ import URLSearchParams from '../url-search-params/URLSearchParams';
 import HTMLCollection from '../nodes/element/HTMLCollection';
 import NodeList from '../nodes/node/NodeList';
 import Selection from '../selection/Selection';
+import IEventTarget from '../event/IEventTarget';
 
 /**
  * Window.
  */
-export default interface IWindow {
+export default interface IWindow extends IEventTarget {
 	// Public Properties
 	readonly happyDOM: {
 		whenAsyncComplete: () => Promise<void>;


Test patch:
diff --git a/packages/happy-dom/test/dom-token-list/DOMTokenList.test.ts b/packages/happy-dom/test/dom-token-list/DOMTokenList.test.ts
new file mode 100644
index 00000000..327450cb
--- /dev/null
+++ b/packages/happy-dom/test/dom-token-list/DOMTokenList.test.ts
@@ -0,0 +1,206 @@
+import IWindow from '../../src/window/IWindow';
+import Window from '../../src/window/Window';
+import IDocument from '../../src/nodes/document/IDocument';
+import IElement from '../../src/nodes/element/IElement';
+import IDOMTokenList from '../../src/dom-token-list/IDOMTokenList';
+
+describe('DOMTokenList', () => {
+	let window: IWindow;
+	let document: IDocument;
+	let element: IElement;
+	let classList: IDOMTokenList;
+
+	beforeEach(() => {
+		window = new Window();
+		document = window.document;
+		element = document.createElement('div');
+		classList = element.classList;
+	});
+
+	describe('add()', () => {
+		it('Adds a class to the list.', () => {
+			classList.add('class1');
+
+			expect(element.className).toBe('class1');
+
+			classList.add('class2');
+			classList.add('class3');
+
+			expect(element.className).toBe('class1 class2 class3');
+
+			classList.add('class2');
+			classList.add('class3');
+
+			expect(element.className).toBe('class1 class2 class3');
+		});
+
+		it('Adds multiple classes to the list.', () => {
+			classList.add('class1', 'class2', 'class3');
+			expect(element.className).toBe('class1 class2 class3');
+		});
+	});
+
+	describe('get length()', () => {
+		it('Is an integer representing the number of objects stored in the object.', () => {
+			expect(classList.length).toBe(0);
+
+			classList.add('class1');
+
+			expect(classList.length).toBe(1);
+			expect(classList[0]).toBe('class1');
+
+			classList.add('class2');
+
+			expect(classList.length).toBe(2);
+			expect(classList[0]).toBe('class1');
+			expect(classList[1]).toBe('class2');
+
+			classList.value = 'otherClass';
+
+			expect(classList.length).toBe(1);
+			expect(classList[0]).toBe('otherClass');
+			expect(classList[1]).toBe(undefined);
+
+			classList.value = '';
+
+			expect(classList.length).toBe(0);
+			expect(classList[0]).toBe(undefined);
+		});
+	});
+
+	describe('get value()', () => {
+		it('Returns the attribute value.', () => {
+			classList.add('class1');
+			classList.add('class2');
+			classList.add('class3');
+			expect(classList.value).toBe('class1 class2 class3');
+
+			classList.add('class1');
+			classList.add('class2');
+			expect(classList.value).toBe('class1 class2 class3');
+		});
+	});
+
+	describe('set value()', () => {
+		it('Sets the attribute value.', () => {
+			classList.value = 'class1 class2 class3';
+			expect(element.className).toBe('class1 class2 class3');
+		});
+	});
+
+	describe('item()', () => {
+		it('Returns an item in the list.', () => {
+			classList.add('class1');
+			expect(classList.item(0)).toBe('class1');
+			expect(classList.item('0')).toBe('class1');
+			expect(classList.item('a')).toBe('class1');
+		});
+	});
+
+	describe('replace()', () => {
+		it('Replaces the token with another one.', () => {
+			classList.add('class1');
+			classList.add('class2');
+			classList.add('class3');
+			expect(classList.replace('class1', 'class4')).toBe(true);
+			expect(element.className).toBe('class4 class2 class3');
+		});
+	});
+
+	describe('remove()', () => {
+		it('Removes a class from the list.', () => {
+			classList.add('class1');
+			classList.remove('class1');
+			expect(element.className).toBe('');
+		});
+	});
+
+	describe('contains()', () => {
+		it('Returns "true" if the list contains a class.', () => {
+			classList.add('class1');
+			expect(classList.contains('class1')).toBe(true);
+			classList.add('class2');
+			expect(classList.contains('class1')).toBe(true);
+			expect(classList.contains('class2')).toBe(true);
+		});
+	});
+
+	describe('toggle()', () => {
+		it('Adds a class from the list when not existing.', () => {
+			expect(classList.toggle('class1')).toBe(true);
+			expect(element.className).toBe('class1');
+		});
+
+		it('Adds a class from the list when force is set.', () => {
+			classList.add('class1');
+			expect(classList.toggle('class1', true)).toBe(true);
+			expect(classList.toggle('class2', true)).toBe(true);
+			expect(element.className).toBe('class1 class2');
+		});
+
+		it('Removes a class from the list when existing.', () => {
+			classList.add('class1');
+			expect(classList.toggle('class1')).toBe(false);
+			expect(element.className).toBe('');
+		});
+
+		it('Adds a class from the list when force is set.', () => {
+			classList.add('class1');
+			expect(classList.toggle('class1', false)).toBe(false);
+			expect(classList.toggle('class2', false)).toBe(false);
+			expect(element.className).toBe('');
+		});
+	});
+
+	describe('values()', () => {
+		it('A stringifier property that returns the value of the list as a string.', () => {
+			element.className = 'class1 class2 class3';
+			expect(Array.from(classList.values())).toEqual(['class1', 'class2', 'class3']);
+		});
+	});
+
+	describe('entries()', () => {
+		it('Returns an iterator, allowing you to go through all key/value pairs contained in this object.', () => {
+			element.className = 'class1 class2 class3';
+			expect(Array.from(classList.entries())).toEqual([
+				[0, 'class1'],
+				[1, 'class2'],
+				[2, 'class3']
+			]);
+		});
+	});
+
+	describe('forEach()', () => {
+		it('Executes a provided callback function once for each DOMTokenList element.', () => {
+			const items = [];
+
+			element.className = 'class1 class2 class3';
+
+			classList.forEach((token, index) => {
+				items.push({ token, index });
+			});
+
+			expect(items).toEqual([
+				{
+					index: 0,
+					token: 'class1'
+				},
+				{
+					index: 1,
+					token: 'class2'
+				},
+				{
+					index: 2,
+					token: 'class3'
+				}
+			]);
+		});
+	});
+
+	describe('keys()', () => {
+		it('Returns an iterator, allowing you to go through all keys of the key/value pairs contained in this object.', () => {
+			element.className = 'class1 class2 class3';
+			expect(Array.from(classList.keys())).toEqual([0, 1, 2]);
+		});
+	});
+});
diff --git a/packages/happy-dom/test/nodes/document/Document.test.ts b/packages/happy-dom/test/nodes/document/Document.test.ts
index 7893cbb4..7dd257df 100644
--- a/packages/happy-dom/test/nodes/document/Document.test.ts
+++ b/packages/happy-dom/test/nodes/document/Document.test.ts
@@ -1,4 +1,5 @@
 import Window from '../../../src/window/Window';
+import IWindow from '../../../src/window/IWindow';
 import CustomElement from '../../CustomElement';
 import HTMLElement from '../../../src/nodes/html-element/HTMLElement';
 import Text from '../../../src/nodes/text/Text';
@@ -6,7 +7,7 @@ import Comment from '../../../src/nodes/comment/Comment';
 import DocumentFragment from '../../../src/nodes/document-fragment/DocumentFragment';
 import TreeWalker from '../../../src/tree-walker/TreeWalker';
 import Node from '../../../src/nodes/node/Node';
-import Document from '../../../src/nodes/document/Document';
+import IDocument from '../../../src/nodes/document/IDocument';
 import Element from '../../../src/nodes/element/Element';
 import Event from '../../../src/event/Event';
 import SVGSVGElement from '../../../src/nodes/svg-element/SVGSVGElement';
@@ -30,8 +31,8 @@ import CustomEvent from '../../../src/event/events/CustomEvent';
 import Selection from '../../../src/selection/Selection';
 
 describe('Document', () => {
-	let window: Window;
-	let document: Document;
+	let window: IWindow;
+	let document: IDocument;
 
 	beforeEach(() => {
 		window = new Window();
@@ -55,6 +56,26 @@ describe('Document', () => {
 		});
 	});
 
+	describe('get scripts()', () => {
+		it('Returns script elements.', () => {
+			const div = document.createElement('div');
+			const span1 = document.createElement('span');
+			const span2 = document.createElement('span');
+			const script1 = document.createElement('script');
+			const script2 = document.createElement('script');
+
+			span1.appendChild(script1);
+			span2.appendChild(script2);
+
+			div.appendChild(span1);
+			div.appendChild(span2);
+
+			document.body.appendChild(div);
+
+			expect(Array.from(document.scripts)).toEqual([script1, script2]);
+		});
+	});
+
 	describe('get childElementCount()', () => {
 		it('Returns child element count.', () => {
 			document.appendChild(document.createElement('div'));
diff --git a/packages/happy-dom/test/nodes/element/DOMTokenList.test.ts b/packages/happy-dom/test/nodes/element/DOMTokenList.test.ts
deleted file mode 100644
index 6fdebfd0..00000000
--- a/packages/happy-dom/test/nodes/element/DOMTokenList.test.ts
+++ /dev/null
@@ -1,167 +0,0 @@
-import Window from '../../../src/window/Window';
-import DOMTokenList from '../../../src/nodes/element/DOMTokenList';
-
-describe('DOMTokenList', () => {
-	let window;
-	let document;
-	let element;
-	let classList;
-
-	beforeEach(() => {
-		window = new Window();
-		document = window.document;
-		element = document.createElement('div');
-		classList = new DOMTokenList(element);
-	});
-
-	describe('add()', () => {
-		it('Adds a class to the list.', () => {
-			classList.add('class');
-			expect(element.className).toBe('class');
-			classList.add('class2');
-			classList.add('class3');
-			expect(element.className).toBe('class class2 class3');
-		});
-		it('Adds multiple classes to the list.', () => {
-			classList.add('class1');
-			classList.add('class2');
-			expect(element.className).toBe('class1 class2');
-		});
-	});
-
-	describe('length', () => {
-		it('Is an integer representing the number of objects stored in the object.', () => {
-			expect(classList.length).toBe(0);
-			classList.add('class1');
-			expect(classList.length).toBe(1);
-		});
-	});
-
-	describe('value', () => {
-		it('A stringifier property that returns the value of the list as a string.', () => {
-			classList.add('class');
-			classList.add('class1');
-			classList.add('class2');
-			expect(classList.value).toBe('class class1 class2');
-
-			classList.add('class1');
-			classList.add('class2');
-			expect(classList.value).toBe('class class1 class2');
-		});
-		it('Set Value.', () => {
-			classList.value = 'class class1 class2';
-			expect(classList.length).toBe(3);
-			expect(classList.value).toBe('class class1 class2');
-		});
-	});
-
-	describe('item()', () => {
-		it('Returns an item in the list.', () => {
-			classList.add('class');
-			expect(classList.item(0)).toBe('class');
-			expect(classList.item('0')).toBe('class');
-			expect(classList.item('a')).toBe(null);
-		});
-	});
-
-	describe('replace()', () => {
-		it('Replaces the token with another one.', () => {
-			classList.add('class');
-			classList.add('class1');
-			classList.add('class3');
-			expect(classList.replace('class', 'class4')).toBe(true);
-			expect(classList.contains('class4')).toBe(true);
-		});
-	});
-
-	describe('remove()', () => {
-		it('Removes a class from the list.', () => {
-			classList.add('class');
-			classList.remove('class');
-			expect(element.className).toBe('');
-		});
-	});
-
-	describe('contains()', () => {
-		it('Returns "true" if the list contains a class.', () => {
-			classList.add('class');
-			expect(classList.contains('class')).toBe(true);
-		});
-	});
-
-	describe('toggle()', () => {
-		it('Adds a class from the list when not existing.', () => {
-			expect(classList.toggle('class')).toBe(true);
-			expect(element.className).toBe('class');
-		});
-		it('Adds a class from the list when force is set.', () => {
-			classList.add('classA');
-			expect(classList.toggle('classA', true)).toBe(true);
-			expect(classList.toggle('classB', true)).toBe(true);
-			expect(element.className).toBe('classA classB');
-		});
-		it('Removes a class from the list when existing.', () => {
-			classList.add('class');
-			expect(classList.toggle('class')).toBe(false);
-			expect(element.className).toBe('');
-		});
-		it('Adds a class from the list when force is set.', () => {
-			classList.add('classA');
-			expect(classList.toggle('classA', false)).toBe(false);
-			expect(classList.toggle('classB', false)).toBe(false);
-			expect(element.className).toBe('');
-		});
-	});
-
-	describe('values()', () => {
-		it('A stringifier property that returns the value of the list as a string.', () => {
-			const classNames = ['class', 'class1', 'class2'];
-			for (const className of classNames) {
-				classList.add(className);
-			}
-
-			for (const value of classList.values()) {
-				const index = classNames.indexOf(value);
-				expect(classList.item(index)).toBe(value);
-			}
-		});
-	});
-
-	describe('entries()', () => {
-		it('Returns an iterator, allowing you to go through all key/value pairs contained in this object.', () => {
-			const classNames = ['class', 'class1', 'class2'];
-			for (const className of classNames) {
-				classList.add(className);
-			}
-
-			for (const [key, value] of classList.entries()) {
-				expect(classList.item(key)).toBe(value);
-			}
-		});
-	});
-
-	describe('forEach()', () => {
-		it('Executes a provided callback function once for each DOMTokenList element.', () => {
-			const classNames = ['class', 'class1', 'class2'];
-			for (const className of classNames) {
-				classList.add(className);
-			}
-			classList.forEach((currentValue, currentIndex, _) => {
-				expect(classList.item(currentIndex)).toBe(currentValue);
-			}, this);
-		});
-	});
-
-	describe('keys()', () => {
-		it('Returns an iterator, allowing you to go through all keys of the key/value pairs contained in this object.', () => {
-			const classNames = ['class', 'class1', 'class2'];
-			for (const className of classNames) {
-				classList.add(className);
-			}
-
-			for (const key of classList.keys()) {
-				expect(classList.item(key)).toBe(classNames[key]);
-			}
-		});
-	});
-});
diff --git a/packages/happy-dom/test/nodes/element/Element.test.ts b/packages/happy-dom/test/nodes/element/Element.test.ts
index 2b4a8d8b..f9b388db 100644
--- a/packages/happy-dom/test/nodes/element/Element.test.ts
+++ b/packages/happy-dom/test/nodes/element/Element.test.ts
@@ -84,6 +84,13 @@ describe('Element', () => {
 		});
 	});
 
+	describe('get classList()', () => {
+		it('Returns a DOMTokenList object.', () => {
+			element.setAttribute('class', 'class1');
+			expect(element.classList.value).toBe('class1');
+		});
+	});
+
 	describe('set id()', () => {
 		it('Sets the element "class" as an attribute.', () => {
 			element.className = 'class';
diff --git a/packages/happy-dom/test/nodes/html-link-element/HTMLLinkElement.test.ts b/packages/happy-dom/test/nodes/html-link-element/HTMLLinkElement.test.ts
index e4dea9e7..7df4dc60 100644
--- a/packages/happy-dom/test/nodes/html-link-element/HTMLLinkElement.test.ts
+++ b/packages/happy-dom/test/nodes/html-link-element/HTMLLinkElement.test.ts
@@ -1,6 +1,6 @@
 import Window from '../../../src/window/Window';
 import Document from '../../../src/nodes/document/Document';
-import HTMLLinkElement from '../../../src/nodes/html-link-element/HTMLLinkElement';
+import IHTMLLinkElement from '../../../src/nodes/html-link-element/IHTMLLinkElement';
 import ResourceFetcher from '../../../src/fetch/ResourceFetcher';
 
 describe('HTMLLinkElement', () => {
@@ -28,7 +28,7 @@ describe('HTMLLinkElement', () => {
 	]) {
 		describe(`get ${property}()`, () => {
 			it(`Returns the "${property}" attribute.`, () => {
-				const element = document.createElement('link');
+				const element = <IHTMLLinkElement>document.createElement('link');
 				element.setAttribute(property, 'test');
 				expect(element[property]).toBe('test');
 			});
@@ -36,16 +36,24 @@ describe('HTMLLinkElement', () => {
 
 		describe(`set ${property}()`, () => {
 			it(`Sets the attribute "${property}".`, () => {
-				const element = document.createElement('link');
+				const element = <IHTMLLinkElement>document.createElement('link');
 				element[property] = 'test';
 				expect(element.getAttribute(property)).toBe('test');
 			});
 		});
 	}
 
+	describe('get relList()', () => {
+		it('Returns a DOMTokenList object.', () => {
+			const element = <IHTMLLinkElement>document.createElement('link');
+			element.setAttribute('rel', 'value1 value2');
+			expect(element.relList.value).toBe('value1 value2');
+		});
+	});
+
 	describe('get href()', () => {
 		it('Returns the "href" attribute.', () => {
-			const element = <HTMLLinkElement>document.createElement('link');
+			const element = <IHTMLLinkElement>document.createElement('link');
 			element.setAttribute('href', 'test');
 			expect(element.href).toBe('test');
 		});
@@ -53,13 +61,13 @@ describe('HTMLLinkElement', () => {
 
 	describe('set href()', () => {
 		it('Sets the attribute "href".', () => {
-			const element = <HTMLLinkElement>document.createElement('link');
+			const element = <IHTMLLinkElement>document.createElement('link');
 			element.href = 'test';
 			expect(element.getAttribute('href')).toBe('test');
 		});
 
 		it('Loads and evaluates an external CSS file when the attribute "href" and "rel" is set and the element is connected to DOM.', done => {
-			const element = <HTMLLinkElement>document.createElement('link');
+			const element = <IHTMLLinkElement>document.createElement('link');
 			const css = 'div { background: red; }';
 			let loadedOptions = null;
 			let loadEvent = null;
@@ -90,7 +98,7 @@ describe('HTMLLinkElement', () => {
 		});
 
 		it('Triggers error event when fetching a CSS file fails during setting the "href" and "rel" attributes.', done => {
-			const element = <HTMLLinkElement>document.createElement('link');
+			const element = <IHTMLLinkElement>document.createElement('link');
 			const thrownError = new Error('error');
 			let errorEvent = null;
 
@@ -115,7 +123,7 @@ describe('HTMLLinkElement', () => {
 		});
 
 		it('Does not load and evaluate external CSS files if the element is not connected to DOM.', () => {
-			const element = <HTMLLinkElement>document.createElement('link');
+			const element = <IHTMLLinkElement>document.createElement('link');
 			const css = 'div { background: red; }';
 			let loadedOptions = null;
 
@@ -133,7 +141,7 @@ describe('HTMLLinkElement', () => {
 
 	describe('set isConnected()', () => {
 		it('Loads and evaluates an external script when "href" attribute has been set, but does not evaluate text content.', done => {
-			const element = <HTMLLinkElement>document.createElement('link');
+			const element = <IHTMLLinkElement>document.createElement('link');
 			const css = 'div { background: red; }';
 			let loadEvent = null;
 			let loadedOptions = null;
@@ -163,7 +171,7 @@ describe('HTMLLinkElement', () => {
 		});
 
 		it('Triggers error event when fetching a CSS file fails while appending the element to the document.', done => {
-			const element = <HTMLLinkElement>document.createElement('link');
+			const element = <IHTMLLinkElement>document.createElement('link');
 			const thrownError = new Error('error');
 			let errorEvent = null;
 
@@ -187,7 +195,7 @@ describe('HTMLLinkElement', () => {
 		});
 
 		it('Does not load external scripts when "href" attribute has been set if the element is not connected to DOM.', () => {
-			const element = <HTMLLinkElement>document.createElement('link');
+			const element = <IHTMLLinkElement>document.createElement('link');
 			const css = 'div { background: red; }';
 			let loadedOptions = null;