Repository: capricorn86/happy-dom
Pull request #339 (2022-01-24 23:55:48)

#327@minor: Adds support for Window.HTMLCollection and Window.NodeList.


Patch:
diff --git a/packages/happy-dom/src/nodes/element/HTMLCollection.ts b/packages/happy-dom/src/nodes/element/HTMLCollection.ts
new file mode 100644
index 00000000..f204c510
--- /dev/null
+++ b/packages/happy-dom/src/nodes/element/HTMLCollection.ts
@@ -0,0 +1,16 @@
+import IHTMLCollection from './IHTMLCollection';
+import INode from '../node/INode';
+
+/**
+ * Class list.
+ */
+export default class HTMLCollection extends Array implements IHTMLCollection<INode> {
+	/**
+	 * Returns item by index.
+	 *
+	 * @param index Index.
+	 */
+	public item(index: number): INode {
+		return index >= 0 && this[index] ? this[index] : null;
+	}
+}
diff --git a/packages/happy-dom/src/nodes/node/INode.ts b/packages/happy-dom/src/nodes/node/INode.ts
index ddae7aa8..9bfaa3c9 100644
--- a/packages/happy-dom/src/nodes/node/INode.ts
+++ b/packages/happy-dom/src/nodes/node/INode.ts
@@ -1,13 +1,14 @@
 import IEventTarget from '../../event/IEventTarget';
 import IDocument from '../document/IDocument';
 import IElement from '../element/IElement';
+import INodeList from './INodeList';
 
 export default interface INode extends IEventTarget {
 	readonly ownerDocument: IDocument;
 	readonly parentNode: INode;
 	readonly parentElement: IElement;
 	readonly nodeType: number;
-	readonly childNodes: INode[];
+	readonly childNodes: INodeList<INode>;
 	readonly isConnected: boolean;
 	readonly nodeName: string;
 	readonly previousSibling: INode;
diff --git a/packages/happy-dom/src/nodes/node/NodeList.ts b/packages/happy-dom/src/nodes/node/NodeList.ts
new file mode 100644
index 00000000..9ba580ed
--- /dev/null
+++ b/packages/happy-dom/src/nodes/node/NodeList.ts
@@ -0,0 +1,16 @@
+import INodeList from './INodeList';
+import INode from './INode';
+
+/**
+ * Class list.
+ */
+export default class NodeList extends Array implements INodeList<INode> {
+	/**
+	 * Returns item by index.
+	 *
+	 * @param index Index.
+	 */
+	public item(index: number): INode {
+		return index >= 0 && this[index] ? this[index] : null;
+	}
+}
diff --git a/packages/happy-dom/src/window/IWindow.ts b/packages/happy-dom/src/window/IWindow.ts
index 3fb6ef9f..25dd5a91 100644
--- a/packages/happy-dom/src/window/IWindow.ts
+++ b/packages/happy-dom/src/window/IWindow.ts
@@ -18,6 +18,7 @@ import SVGElement from '../nodes/svg-element/SVGElement';
 import HTMLScriptElement from '../nodes/html-script-element/HTMLScriptElement';
 import HTMLImageElement from '../nodes/html-image-element/HTMLImageElement';
 import DocumentFragment from '../nodes/document-fragment/DocumentFragment';
+import CharacterData from '../nodes/character-data/CharacterData';
 import TreeWalker from '../tree-walker/TreeWalker';
 import Event from '../event/Event';
 import CustomEvent from '../event/events/CustomEvent';
@@ -38,6 +39,7 @@ import DOMException from '../exception/DOMException';
 import FileReader from '../file/FileReader';
 import History from '../history/History';
 import CSSStyleDeclaration from '../css/CSSStyleDeclaration';
+import PointerEvent from '../event/events/PointerEvent';
 import MouseEvent from '../event/events/MouseEvent';
 import FocusEvent from '../event/events/FocusEvent';
 import WheelEvent from '../event/events/WheelEvent';
@@ -56,6 +58,9 @@ import HTMLStyleElement from '../nodes/html-style-element/HTMLStyleElement';
 import IFetchOptions from './IFetchOptions';
 import NodeFilter from '../tree-walker/NodeFilter';
 import Window from './Window';
+import URLSearchParams from '../url-search-params/URLSearchParams';
+import HTMLCollection from '../nodes/element/HTMLCollection';
+import NodeList from '../nodes/node/NodeList';
 
 /**
  * Window.
@@ -86,6 +91,7 @@ export default interface IWindow {
 	readonly ShadowRoot: typeof ShadowRoot;
 	readonly Element: typeof Element;
 	readonly DocumentFragment: typeof DocumentFragment;
+	readonly CharacterData: typeof CharacterData;
 	readonly NodeFilter: typeof NodeFilter;
 	readonly TreeWalker: typeof TreeWalker;
 	readonly DOMParser: typeof DOMParser;
@@ -99,6 +105,7 @@ export default interface IWindow {
 	readonly CustomEvent: typeof CustomEvent;
 	readonly AnimationEvent: typeof AnimationEvent;
 	readonly KeyboardEvent: typeof KeyboardEvent;
+	readonly PointerEvent: typeof PointerEvent;
 	readonly MouseEvent: typeof MouseEvent;
 	readonly FocusEvent: typeof FocusEvent;
 	readonly WheelEvent: typeof WheelEvent;
@@ -124,6 +131,9 @@ export default interface IWindow {
 	readonly History: typeof History;
 	readonly Screen: typeof Screen;
 	readonly Storage: typeof Storage;
+	readonly URLSearchParams: typeof URLSearchParams;
+	readonly HTMLCollection: typeof HTMLCollection;
+	readonly NodeList: typeof NodeList;
 
 	// Events
 	onload: (event: Event) => void;
diff --git a/packages/happy-dom/src/window/Window.ts b/packages/happy-dom/src/window/Window.ts
index fbb5d1ce..c326ebf1 100644
--- a/packages/happy-dom/src/window/Window.ts
+++ b/packages/happy-dom/src/window/Window.ts
@@ -63,6 +63,8 @@ import HTMLStyleElement from '../nodes/html-style-element/HTMLStyleElement';
 import IFetchOptions from './IFetchOptions';
 import IWindow from './IWindow';
 import URLSearchParams from '../url-search-params/URLSearchParams';
+import HTMLCollection from '../nodes/element/HTMLCollection';
+import NodeList from '../nodes/node/NodeList';
 
 const FETCH_RESPONSE_TYPE_METHODS = ['blob', 'json', 'text'];
 
@@ -140,6 +142,8 @@ export default class Window extends EventTarget implements IWindow, NodeJS.Globa
 	public readonly Screen = Screen;
 	public readonly Storage = Storage;
 	public readonly URLSearchParams = URLSearchParams;
+	public readonly HTMLCollection = HTMLCollection;
+	public readonly NodeList = NodeList;
 
 	// Events
 	public onload: (event: Event) => void = null;


Test patch:
diff --git a/packages/happy-dom/test/nodes/element/HTMLCollection.test.ts b/packages/happy-dom/test/nodes/element/HTMLCollection.test.ts
new file mode 100644
index 00000000..e9ce4231
--- /dev/null
+++ b/packages/happy-dom/test/nodes/element/HTMLCollection.test.ts
@@ -0,0 +1,27 @@
+import Window from '../../../src/window/Window';
+import Document from '../../../src/nodes/document/Document';
+
+describe('HTMLCollection', () => {
+	let window: Window;
+	let document: Document;
+
+	beforeEach(() => {
+		window = new Window();
+		document = window.document;
+	});
+
+	afterEach(() => {
+		jest.restoreAllMocks();
+	});
+
+	describe('item()', () => {
+		it('Returns node at index.', () => {
+			const div = document.createElement('div');
+			const span = document.createElement('span');
+			document.body.appendChild(div);
+			document.body.appendChild(span);
+			expect(document.body.childNodes.item(0) === div).toBe(true);
+			expect(document.body.childNodes.item(1) === span).toBe(true);
+		});
+	});
+});
diff --git a/packages/happy-dom/test/nodes/node/NodeList.test.ts b/packages/happy-dom/test/nodes/node/NodeList.test.ts
new file mode 100644
index 00000000..39dd2b09
--- /dev/null
+++ b/packages/happy-dom/test/nodes/node/NodeList.test.ts
@@ -0,0 +1,27 @@
+import Window from '../../../src/window/Window';
+import Document from '../../../src/nodes/document/Document';
+
+describe('NodeList', () => {
+	let window: Window;
+	let document: Document;
+
+	beforeEach(() => {
+		window = new Window();
+		document = window.document;
+	});
+
+	afterEach(() => {
+		jest.restoreAllMocks();
+	});
+
+	describe('item()', () => {
+		it('Returns node at index.', () => {
+			const text = document.createTextNode('test');
+			const comment = document.createComment('test');
+			document.body.appendChild(text);
+			document.body.appendChild(comment);
+			expect(document.body.childNodes.item(0) === text).toBe(true);
+			expect(document.body.childNodes.item(1) === comment).toBe(true);
+		});
+	});
+});