Repository: capricorn86/happy-dom
Pull request #381 (2022-02-25 15:53:06)

#380@minor: Adds support for DOMTokenList. Adds support for window.globalThis, document.scripts.
solve #380 (maybe)

not impl section: `classList[0]`、`classList.supports('add-scripts')`

I don't think this good enough, so you can modify or even rejection this pr.

test code section:

```javascript
const { Window } = require("./packages/happy-dom");

const window = new Window();
const document = window.document;

document.body.innerHTML = '<div class="container b a s d x a z"></div>';

const container = document.querySelector('.container');
const button = document.createElement('button');
container.appendChild(button);

let classList = container.classList;

console.log(classList.value);
classList.add("b");
console.log(classList.value);

console.log(classList.length);
classList.length = 1;
console.log(classList.length);

console.log(classList.contains('b'));

console.log(classList.value);
classList.remove("b");
console.log(classList.value);

console.log(classList.contains('b'));

console.log(classList.value);
for (const [key, value] of classList.entries()) {
	console.log(key, value);
}


for (const key of classList.keys()) {
	console.log(key);
}

for (const value of classList.values()) {
	console.log(value);
}

console.log(classList.item(0));
console.log(classList.item('0'));
console.log(classList.item('a'));


classList.forEach((currentValue, currentIndex, listObj) => {
	console.log(currentValue, currentIndex, listObj);
}, this);


console.log(classList.value);
console.log(classList.replace("a"));
console.log(classList.value);

console.log(classList.supports("add-scripts"));

```

check pass image: 

![image](https://user-images.githubusercontent.com/60805843/155745344-fcab0edc-e1d7-4831-a975-0ed5aef9a016.png)



Patch:
diff --git a/packages/happy-dom/src/nodes/document/Document.ts b/packages/happy-dom/src/nodes/document/Document.ts
index 0bfa4e3e..b69dbe93 100644
--- a/packages/happy-dom/src/nodes/document/Document.ts
+++ b/packages/happy-dom/src/nodes/document/Document.ts
@@ -240,6 +240,15 @@ export default class Document extends Node implements IDocument {
 		return this._defaultView.location;
 	}
 
+	/**
+	 * Returns scripts.
+	 *
+	 * @returns Scripts.
+	 */
+	public get scripts(): IHTMLCollection<IElement> {
+		return this.getElementsByTagName('script');
+	}
+
 	/**
 	 * 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/element/ClassList.ts b/packages/happy-dom/src/nodes/element/ClassList.ts
deleted file mode 100644
index 56a0bea3..00000000
--- a/packages/happy-dom/src/nodes/element/ClassList.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-import DOMException from '../../exception/DOMException';
-import Element from './Element';
-
-/**
- * Class list.
- */
-export default class ClassList {
-	private _ownerElement: Element;
-
-	/**
-	 * Adds class names.
-	 *
-	 * @param ownerElement Owner element.
-	 */
-	constructor(ownerElement: Element) {
-		this._ownerElement = ownerElement;
-	}
-
-	/**
-	 * Adds class names.
-	 *
-	 * @param classNames Class names.
-	 */
-	public add(...classNames: string[]): void {
-		const attr = this._ownerElement.getAttribute('class');
-		const list = attr ? attr.split(' ') : [];
-		for (const className of classNames) {
-			if (!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.`
-					);
-				}
-				list.push(className);
-			}
-		}
-		this._ownerElement.setAttribute('class', list.join(' '));
-	}
-
-	/**
-	 * Adds class names.
-	 *
-	 * @param classNames Class names.
-	 */
-	public remove(...classNames: string[]): void {
-		const attr = this._ownerElement.getAttribute('class');
-		const list = attr ? attr.split(' ') : [];
-		for (const className of classNames) {
-			const index = list.indexOf(className);
-			if (index !== -1) {
-				list.splice(index, 1);
-			}
-		}
-		this._ownerElement.setAttribute('class', 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.getAttribute('class');
-		const list = attr ? attr.split(' ') : [];
-		return 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;
-	}
-}
diff --git a/packages/happy-dom/src/nodes/element/DOMTokenList.ts b/packages/happy-dom/src/nodes/element/DOMTokenList.ts
new file mode 100644
index 00000000..90a37de0
--- /dev/null
+++ b/packages/happy-dom/src/nodes/element/DOMTokenList.ts
@@ -0,0 +1,201 @@
+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 69b00201..fe1f00d7 100644
--- a/packages/happy-dom/src/nodes/element/Element.ts
+++ b/packages/happy-dom/src/nodes/element/Element.ts
@@ -3,7 +3,7 @@ import ShadowRoot from '../shadow-root/ShadowRoot';
 import Attr from '../../attribute/Attr';
 import DOMRect from './DOMRect';
 import Range from './Range';
-import ClassList from './ClassList';
+import DOMTokenList from './DOMTokenList';
 import QuerySelector from '../../query-selector/QuerySelector';
 import SelectorItem from '../../query-selector/SelectorItem';
 import MutationRecord from '../../mutation-observer/MutationRecord';
@@ -36,11 +36,12 @@ export default class Element extends Node implements IElement {
 	public tagName: string = null;
 	public nodeType = Node.ELEMENT_NODE;
 	public shadowRoot: IShadowRoot = null;
-	public readonly classList = new ClassList(this);
+	public _attributes: { [k: string]: Attr } = {};
+	public readonly classList = new DOMTokenList(this);
 	public scrollTop = 0;
 	public scrollLeft = 0;
 	public children: IHTMLCollection<IElement> = HTMLCollectionFactory.create();
-	public _attributes: { [k: string]: Attr } = {};
+
 	public readonly namespaceURI: string = null;
 
 	/**
diff --git a/packages/happy-dom/src/nodes/element/IDOMTokenList.ts b/packages/happy-dom/src/nodes/element/IDOMTokenList.ts
new file mode 100644
index 00000000..502fbe11
--- /dev/null
+++ b/packages/happy-dom/src/nodes/element/IDOMTokenList.ts
@@ -0,0 +1,17 @@
+/**
+ * IDOMTokenList.
+ */
+export default interface IDOMTokenList<T> {
+	item(index: number): string;
+	contains(token: string): boolean;
+	add(...tokens: string[]): void;
+	remove(...tokens: string[]): void;
+	toggle(token: string, force?: boolean): boolean;
+	replace(token: string, newToken: string): boolean;
+	supports(token: string): boolean;
+
+	values(): IterableIterator<T>;
+	entries(): IterableIterator<[number, T]>;
+	forEach(callback: (currentValue, currentIndex, listObj) => void, thisArg?: this): void;
+	keys(): IterableIterator<number>;
+}
diff --git a/packages/happy-dom/src/nodes/element/IElement.ts b/packages/happy-dom/src/nodes/element/IElement.ts
index c224a6ad..0412cfc5 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 ClassList from './ClassList';
+import DOMTokenList from './DOMTokenList';
 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: ClassList;
+	readonly classList: DOMTokenList;
 	readonly namespaceURI: string;
 	scrollTop: number;
 	scrollLeft: number;
diff --git a/packages/happy-dom/src/window/IWindow.ts b/packages/happy-dom/src/window/IWindow.ts
index ed6eeedc..92b14d1c 100644
--- a/packages/happy-dom/src/window/IWindow.ts
+++ b/packages/happy-dom/src/window/IWindow.ts
@@ -166,6 +166,7 @@ export default interface IWindow {
 	readonly top: IWindow;
 	readonly parent: IWindow;
 	readonly window: IWindow;
+	readonly globalThis: IWindow;
 	readonly screen: Screen;
 	readonly innerWidth: number;
 	readonly innerHeight: number;
diff --git a/packages/happy-dom/src/window/Window.ts b/packages/happy-dom/src/window/Window.ts
index 6a330935..f8ff3e6a 100644
--- a/packages/happy-dom/src/window/Window.ts
+++ b/packages/happy-dom/src/window/Window.ts
@@ -178,6 +178,7 @@ export default class Window extends EventTarget implements IWindow, NodeJS.Globa
 	public readonly top = this;
 	public readonly parent = this;
 	public readonly window = this;
+	public readonly globalThis = this;
 	public readonly screen = new Screen();
 	public readonly innerWidth = 1024;
 	public readonly innerHeight = 768;


Test patch:
diff --git a/packages/happy-dom/test/nodes/element/ClassList.test.ts b/packages/happy-dom/test/nodes/element/ClassList.test.ts
deleted file mode 100644
index b820ff72..00000000
--- a/packages/happy-dom/test/nodes/element/ClassList.test.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import Window from '../../../src/window/Window';
-import ClassList from '../../../src/nodes/element/ClassList';
-
-describe('ClassList', () => {
-	let window;
-	let document;
-	let element;
-	let classList;
-
-	beforeEach(() => {
-		window = new Window();
-		document = window.document;
-		element = document.createElement('div');
-		classList = new ClassList(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('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('');
-		});
-	});
-});
diff --git a/packages/happy-dom/test/nodes/element/DOMTokenList.test.ts b/packages/happy-dom/test/nodes/element/DOMTokenList.test.ts
new file mode 100644
index 00000000..6fdebfd0
--- /dev/null
+++ b/packages/happy-dom/test/nodes/element/DOMTokenList.test.ts
@@ -0,0 +1,167 @@
+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]);
+			}
+		});
+	});
+});