Repository: capricorn86/happy-dom (ts)

Issue:
Elment.attributes does not implement the NamedNodeMap interface
I was trying to replace JSDOM with Happy DOM and the app was failing to run something akin to:
```
const element = document.createElement('div');
const someAttribute = document.createAttribute('someattribute');
element.attributes.setNamedItem(someAttribute);
```

I see the current implementation is:
```
readonly attributes: { [k: string]: Attr | number };
```

Is there any chance there will be a switch to [NamedNodeMap ](https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap)in the future?


Pull request description:
#308@minor: Implement NamedNodeMap and use it for Element.attributes
Hi,

This PR implements the [NamedNodeMap](https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap) interface and makes it available in `Window`. `Element.attributes` now returns a `NamedNodeMap`.

Closes #308 

Patch:
diff --git a/packages/happy-dom/src/named-node-map/INamedNodeMap.ts b/packages/happy-dom/src/named-node-map/INamedNodeMap.ts
new file mode 100644
index 00000000..a0a904fc
--- /dev/null
+++ b/packages/happy-dom/src/named-node-map/INamedNodeMap.ts
@@ -0,0 +1,70 @@
+import IAttr from '../nodes/attr/IAttr';
+
+/**
+ * NamedNodeMap.
+ *
+ * Reference:
+ * https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap.
+ */
+export default interface INamedNodeMap extends Iterable<IAttr> {
+	[index: number]: IAttr;
+	[Symbol.toStringTag]: string;
+	readonly length: number;
+
+	/**
+	 * Returns attribute by index.
+	 *
+	 * @param index Index.
+	 */
+	item: (index: number) => IAttr;
+
+	/**
+	 * Returns attribute by name.
+	 *
+	 * @param qualifiedName Name.
+	 * @returns Attribute.
+	 */
+	getNamedItem: (qualifiedName: string) => IAttr;
+
+	/**
+	 * Returns attribute by name and namespace.
+	 *
+	 * @param namespace Namespace.
+	 * @param localName Local name of the attribute.
+	 * @returns Attribute.
+	 */
+	getNamedItemNS: (namespace: string, localName: string) => IAttr;
+
+	/**
+	 * Adds a new attribute node.
+	 *
+	 * @param attr Attribute.
+	 * @returns Replaced attribute.
+	 */
+	setNamedItem: (attr: IAttr) => IAttr;
+
+	/**
+	 * Adds a new namespaced attribute node.
+	 *
+	 * @param attr Attribute.
+	 * @returns Replaced attribute.
+	 */
+	setNamedItemNS: (attr: IAttr) => IAttr;
+
+	/**
+	 * Removes an attribute.
+	 *
+	 * @param qualifiedName Name of the attribute.
+	 * @returns Removed attribute.
+	 */
+	removeNamedItem: (qualifiedName: string) => IAttr;
+
+	/**
+	 * Removes a namespaced attribute.
+	 *
+	 * @param namespace Namespace.
+	 * @param localName Local name of the attribute.
+	 * @returns Removed attribute.
+	 */
+	removeNamedItemNS: (namespace: string, localName: string) => IAttr;
+}
diff --git a/packages/happy-dom/src/named-node-map/NamedNodeMap.ts b/packages/happy-dom/src/named-node-map/NamedNodeMap.ts
new file mode 100644
index 00000000..19b18968
--- /dev/null
+++ b/packages/happy-dom/src/named-node-map/NamedNodeMap.ts
@@ -0,0 +1,145 @@
+import type Element from '../nodes/element/Element';
+import IAttr from '../nodes/attr/IAttr';
+import INamedNodeMap from './INamedNodeMap';
+
+/**
+ * NamedNodeMap.
+ *
+ * Reference:
+ * https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap.
+ */
+export default class NamedNodeMap implements INamedNodeMap {
+	[index: number]: IAttr;
+
+	/**
+	 * Reference to the element.
+	 */
+	#ownerElement: Element;
+
+	/**
+	 * Constructor.
+	 *
+	 * @param element Associated element.
+	 */
+	constructor(element: Element) {
+		this.#ownerElement = element;
+	}
+
+	/**
+	 * Returns string.
+	 *
+	 * @returns string.
+	 */
+	public get [Symbol.toStringTag](): string {
+		return this.constructor.name;
+	}
+
+	/**
+	 * Length.
+	 *
+	 * @returns Length.
+	 */
+	public get length(): number {
+		return Object.keys(this.#ownerElement._attributes).length;
+	}
+
+	/**
+	 * Returns attribute by index.
+	 *
+	 * @param index Index.
+	 */
+	public item(index: number): IAttr | null {
+		if (index < 0) {
+			return null;
+		}
+		const attr = Object.values(this.#ownerElement._attributes)[index];
+		return attr ? attr : null;
+	}
+
+	/**
+	 * Returns attribute by name.
+	 *
+	 * @param qualifiedName Name.
+	 * @returns Attribute.
+	 */
+	public getNamedItem(qualifiedName: string): IAttr | null {
+		return this.#ownerElement.getAttributeNode(qualifiedName);
+	}
+
+	/**
+	 * Returns attribute by name and namespace.
+	 *
+	 * @param namespace Namespace.
+	 * @param localName Local name of the attribute.
+	 * @returns Attribute.
+	 */
+	public getNamedItemNS(namespace: string, localName: string): IAttr | null {
+		return this.#ownerElement.getAttributeNodeNS(namespace, localName);
+	}
+
+	/**
+	 * Adds a new attribute node.
+	 *
+	 * @param attr Attribute.
+	 * @returns Replaced attribute.
+	 */
+	public setNamedItem(attr: IAttr): IAttr {
+		return this.#ownerElement.setAttributeNode(attr);
+	}
+
+	/**
+	 * Adds a new namespaced attribute node.
+	 *
+	 * @param attr Attribute.
+	 * @returns Replaced attribute.
+	 */
+	public setNamedItemNS(attr: IAttr): IAttr {
+		return this.#ownerElement.setAttributeNodeNS(attr);
+	}
+
+	/**
+	 * Removes an attribute.
+	 *
+	 * @param qualifiedName Name of the attribute.
+	 * @returns Removed attribute.
+	 */
+	public removeNamedItem(qualifiedName: string): IAttr | null {
+		const attr = this.getNamedItem(qualifiedName);
+
+		if (attr) {
+			this.#ownerElement.removeAttributeNode(attr);
+		}
+		return attr;
+	}
+
+	/**
+	 * Removes a namespaced attribute.
+	 *
+	 * @param namespace Namespace.
+	 * @param localName Local name of the attribute.
+	 * @returns Removed attribute.
+	 */
+	public removeNamedItemNS(namespace: string, localName: string): IAttr | null {
+		const attr = this.getNamedItemNS(namespace, localName);
+
+		if (attr) {
+			this.#ownerElement.removeAttributeNode(attr);
+		}
+		return attr;
+	}
+
+	/**
+	 * Iterator.
+	 *
+	 * @returns Iterator.
+	 */
+	public [Symbol.iterator](): Iterator<IAttr> {
+		let index = -1;
+		return {
+			next: () => {
+				index++;
+				return { value: this.item(index), done: index >= this.length };
+			}
+		};
+	}
+}
diff --git a/packages/happy-dom/src/nodes/element/Element.ts b/packages/happy-dom/src/nodes/element/Element.ts
index e41f4c9f..0e90932d 100644
--- a/packages/happy-dom/src/nodes/element/Element.ts
+++ b/packages/happy-dom/src/nodes/element/Element.ts
@@ -1,6 +1,7 @@
 import Node from '../node/Node';
 import ShadowRoot from '../shadow-root/ShadowRoot';
 import Attr from '../attr/Attr';
+import NamedNodeMap from '../../named-node-map/NamedNodeMap';
 import DOMRect from './DOMRect';
 import DOMTokenList from '../../dom-token-list/DOMTokenList';
 import IDOMTokenList from '../../dom-token-list/IDOMTokenList';
@@ -26,6 +27,8 @@ import IText from '../text/IText';
 import IDOMRectList from './IDOMRectList';
 import DOMRectListFactory from './DOMRectListFactory';
 import IAttr from '../attr/IAttr';
+import INamedNodeMap from '../../named-node-map/INamedNodeMap';
+
 import Event from '../../event/Event';
 
 /**
@@ -249,11 +252,8 @@ export default class Element extends Node implements IElement {
 	 *
 	 * @returns Attributes.
 	 */
-	public get attributes(): { [k: string | number]: IAttr } & { length: number } {
-		const attributes = Object.values(this._attributes);
-		return Object.assign({}, this._attributes, attributes, {
-			length: attributes.length
-		});
+	public get attributes(): INamedNodeMap {
+		return Object.assign(new NamedNodeMap(this), Object.values(this._attributes), this._attributes);
 	}
 
 	/**
diff --git a/packages/happy-dom/src/nodes/element/IElement.ts b/packages/happy-dom/src/nodes/element/IElement.ts
index 20c6c765..7649cd0b 100644
--- a/packages/happy-dom/src/nodes/element/IElement.ts
+++ b/packages/happy-dom/src/nodes/element/IElement.ts
@@ -1,5 +1,6 @@
 import IShadowRoot from '../shadow-root/IShadowRoot';
 import IAttr from '../attr/IAttr';
+import INamedNodeMap from '../../named-node-map/INamedNodeMap';
 import DOMRect from './DOMRect';
 import IDOMTokenList from '../../dom-token-list/IDOMTokenList';
 import INode from './../node/INode';
@@ -29,7 +30,7 @@ export default interface IElement extends IChildNode, INonDocumentTypeChildNode,
 	slot: string;
 	readonly nodeName: string;
 	readonly localName: string;
-	readonly attributes: { [k: string | number]: IAttr } & { length: number };
+	readonly attributes: INamedNodeMap;
 
 	// Events
 	oncancel: (event: Event) => void | null;
diff --git a/packages/happy-dom/src/nodes/node/NodeUtility.ts b/packages/happy-dom/src/nodes/node/NodeUtility.ts
index 12e91da9..0cafdcf0 100644
--- a/packages/happy-dom/src/nodes/node/NodeUtility.ts
+++ b/packages/happy-dom/src/nodes/node/NodeUtility.ts
@@ -148,8 +148,8 @@ export default class NodeUtility {
 	 * @param elementB
 	 */
 	public static attributeListsEqual(elementA: IElement, elementB: IElement): boolean {
-		const listA = Object.values(elementA.attributes);
-		const listB = Object.values(elementB.attributes);
+		const listA = <Array<IAttr>>Object.values(elementA['_attributes']);
+		const listB = <Array<IAttr>>Object.values(elementB['_attributes']);
 
 		const lengthA = listA.length;
 		const lengthB = listB.length;
diff --git a/packages/happy-dom/src/window/IWindow.ts b/packages/happy-dom/src/window/IWindow.ts
index 3d9a61eb..94dee772 100644
--- a/packages/happy-dom/src/window/IWindow.ts
+++ b/packages/happy-dom/src/window/IWindow.ts
@@ -95,6 +95,7 @@ import MediaQueryList from '../match-media/MediaQueryList';
 import DOMRect from '../nodes/element/DOMRect';
 import Window from './Window';
 import Attr from '../nodes/attr/Attr';
+import NamedNodeMap from '../named-node-map/NamedNodeMap';
 import { URLSearchParams } from 'url';
 import { Performance } from 'perf_hooks';
 import IElement from '../nodes/element/IElement';
@@ -134,6 +135,7 @@ export default interface IWindow extends IEventTarget, NodeJS.Global {
 	readonly HTMLBaseElement: typeof HTMLBaseElement;
 	readonly HTMLDialogElement: typeof HTMLDialogElement;
 	readonly Attr: typeof Attr;
+	readonly NamedNodeMap: typeof NamedNodeMap;
 	readonly SVGSVGElement: typeof SVGSVGElement;
 	readonly SVGElement: typeof SVGElement;
 	readonly Image: typeof Image;
diff --git a/packages/happy-dom/src/window/Window.ts b/packages/happy-dom/src/window/Window.ts
index 5120800a..ac23a14f 100644
--- a/packages/happy-dom/src/window/Window.ts
+++ b/packages/happy-dom/src/window/Window.ts
@@ -108,6 +108,7 @@ import { Buffer } from 'buffer';
 import Base64 from '../base64/Base64';
 import IDocument from '../nodes/document/IDocument';
 import Attr from '../nodes/attr/Attr';
+import NamedNodeMap from '../named-node-map/NamedNodeMap';
 import IElement from '../nodes/element/IElement';
 import ProcessingInstruction from '../nodes/processing-instruction/ProcessingInstruction';
 
@@ -167,6 +168,7 @@ export default class Window extends EventTarget implements IWindow {
 	public readonly HTMLBaseElement = HTMLBaseElement;
 	public readonly HTMLDialogElement = HTMLDialogElement;
 	public readonly Attr = Attr;
+	public readonly NamedNodeMap = NamedNodeMap;
 	public readonly SVGSVGElement = SVGSVGElement;
 	public readonly SVGElement = SVGElement;
 	public readonly Text = Text;


Test patch:
diff --git a/packages/happy-dom/test/named-node-map/NamedNodeMap.test.ts b/packages/happy-dom/test/named-node-map/NamedNodeMap.test.ts
new file mode 100644
index 00000000..fefa0a22
--- /dev/null
+++ b/packages/happy-dom/test/named-node-map/NamedNodeMap.test.ts
@@ -0,0 +1,154 @@
+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 INamedNodeMap from 'src/named-node-map/INamedNodeMap';
+
+describe('NamedNodeMap', () => {
+	let window: IWindow;
+	let document: IDocument;
+	let element: IElement;
+	let attributes: INamedNodeMap;
+
+	beforeEach(() => {
+		window = new Window();
+		document = window.document;
+		element = document.createElement('div');
+		attributes = element.attributes;
+	});
+
+	describe('get toString()', () => {
+		it('Returns a stirng.', () => {
+			expect(attributes.toString()).toBe('[object NamedNodeMap]');
+		});
+	});
+
+	describe('get toString()', () => {
+		it('Returns a stirng.', () => {
+			expect(attributes.toString()).toBe('[object NamedNodeMap]');
+		});
+	});
+
+	describe('Symbol.iterator()', () => {
+		it('Handles being an iterator.', () => {
+			element.setAttribute('key1', 'value1');
+			element.setAttribute('key2', 'value2');
+
+			const attributeList = [];
+
+			for (const attribute of attributes) {
+				attributeList.push(attribute);
+			}
+
+			expect(attributes.length).toBe(2);
+			expect(attributeList[0].name).toBe('key1');
+			expect(attributeList[0].value).toBe('value1');
+			expect(attributeList[1].name).toBe('key2');
+			expect(attributeList[1].value).toBe('value2');
+
+			element.setAttribute('key3', 'value3');
+
+			expect(attributes.length).toBe(3);
+		});
+	});
+
+	describe('item()', () => {
+		it('Returns an attribute by index.', () => {
+			element.setAttribute('key1', 'value1');
+			element.setAttribute('key2', 'value2');
+
+			expect(attributes.item(0).name).toBe('key1');
+			expect(attributes.item(0).value).toBe('value1');
+			expect(attributes.item(1).name).toBe('key2');
+			expect(attributes.item(1).value).toBe('value2');
+		});
+	});
+
+	describe('getNamedItem()', () => {
+		it('Returns an attribute by name.', () => {
+			element.setAttribute('key1', 'value1');
+			element.setAttribute('key2', 'value2');
+
+			expect(attributes.getNamedItem('key1').name).toBe('key1');
+			expect(attributes.getNamedItem('key1').value).toBe('value1');
+			expect(attributes.getNamedItem('key2').name).toBe('key2');
+			expect(attributes.getNamedItem('key2').value).toBe('value2');
+		});
+	});
+
+	describe('getNamedItemNS()', () => {
+		it('Returns an attribute by name.', () => {
+			element.setAttributeNS('namespace', 'key1', 'value1');
+			element.setAttributeNS('namespace', 'key2', 'value2');
+
+			expect(attributes.getNamedItemNS('namespace', 'key1').name).toBe('key1');
+			expect(attributes.getNamedItemNS('namespace', 'key1').value).toBe('value1');
+			expect(attributes.getNamedItemNS('namespace', 'key2').name).toBe('key2');
+			expect(attributes.getNamedItemNS('namespace', 'key2').value).toBe('value2');
+		});
+	});
+
+	describe('setNamedItem()', () => {
+		it('Adds an attribute when not existing.', () => {
+			element.setAttribute('key', 'value');
+			const attr = attributes.removeNamedItem('key');
+
+			expect(attributes.getNamedItem('key')).toBe(null);
+
+			attributes.setNamedItem(attr);
+
+			expect(attributes.getNamedItem('key')).toBe(attr);
+		});
+
+		it('Replaces an attribute when existing.', () => {
+			element.setAttribute('key', 'value1');
+			const attr = document.createAttributeNS('namespace', 'key');
+			attr.value = 'value2';
+
+			const replaced = attributes.setNamedItem(attr);
+
+			expect(replaced.name).toBe('key');
+			expect(replaced.value).toBe('value1');
+			expect(attributes.getNamedItem('key')).toBe(attr);
+			expect(element.getAttribute('key')).toBe('value2');
+		});
+	});
+
+	describe('setNamedItemNS()', () => {
+		it('Adds an namespaced attribute when not existing.', () => {
+			element.setAttributeNS('namespace', 'key', 'value');
+			const attr = attributes.removeNamedItemNS('namespace', 'key');
+
+			attributes.setNamedItemNS(attr);
+
+			expect(attributes.getNamedItem('key')).toBe(attr);
+			expect(element.getAttributeNS('namespace', 'key')).toBe('value');
+		});
+
+		it('Replaces an attribute when existing.', () => {
+			element.setAttributeNS('namespace', 'key', 'value1');
+			const attr = document.createAttributeNS('namespace', 'key');
+			attr.value = 'value2';
+
+			const replaced = attributes.setNamedItemNS(attr);
+
+			expect(replaced.name).toBe('key');
+			expect(replaced.value).toBe('value1');
+
+			expect(attributes.getNamedItemNS('namespace', 'key')).toBe(attr);
+			expect(element.getAttributeNS('namespace', 'key')).toBe('value2');
+		});
+	});
+
+	describe('removeNamedItem()', () => {
+		it('Removes an attribute from the list.', () => {
+			element.setAttribute('key', 'value');
+			const removed = attributes.removeNamedItem('key');
+
+			expect(removed.name).toBe('key');
+			expect(removed.value).toBe('value');
+
+			expect(element.getAttribute('key')).toBe(null);
+		});
+	});
+});
diff --git a/packages/happy-dom/test/nodes/element/Element.test.ts b/packages/happy-dom/test/nodes/element/Element.test.ts
index f35e87e1..cdb0ca82 100644
--- a/packages/happy-dom/test/nodes/element/Element.test.ts
+++ b/packages/happy-dom/test/nodes/element/Element.test.ts
@@ -267,26 +267,26 @@ describe('Element', () => {
 			expect(element.attributes[2].ownerElement === element).toBe(true);
 			expect(element.attributes[2].ownerDocument === document).toBe(true);
 
-			expect(element.attributes.key1.name).toBe('key1');
-			expect(element.attributes.key1.value).toBe('value1');
-			expect(element.attributes.key1.namespaceURI).toBe(null);
-			expect(element.attributes.key1.specified).toBe(true);
-			expect(element.attributes.key1.ownerElement === element).toBe(true);
-			expect(element.attributes.key1.ownerDocument === document).toBe(true);
-
-			expect(element.attributes.key2.name).toBe('key2');
-			expect(element.attributes.key2.value).toBe('value2');
-			expect(element.attributes.key2.namespaceURI).toBe(null);
-			expect(element.attributes.key2.specified).toBe(true);
-			expect(element.attributes.key2.ownerElement === element).toBe(true);
-			expect(element.attributes.key2.ownerDocument === document).toBe(true);
-
-			expect(element.attributes.key3.name).toBe('key3');
-			expect(element.attributes.key3.value).toBe('value3');
-			expect(element.attributes.key3.namespaceURI).toBe(null);
-			expect(element.attributes.key3.specified).toBe(true);
-			expect(element.attributes.key3.ownerElement === element).toBe(true);
-			expect(element.attributes.key3.ownerDocument === document).toBe(true);
+			expect(element.attributes['key1'].name).toBe('key1');
+			expect(element.attributes['key1'].value).toBe('value1');
+			expect(element.attributes['key1'].namespaceURI).toBe(null);
+			expect(element.attributes['key1'].specified).toBe(true);
+			expect(element.attributes['key1'].ownerElement === element).toBe(true);
+			expect(element.attributes['key1'].ownerDocument === document).toBe(true);
+
+			expect(element.attributes['key2'].name).toBe('key2');
+			expect(element.attributes['key2'].value).toBe('value2');
+			expect(element.attributes['key2'].namespaceURI).toBe(null);
+			expect(element.attributes['key2'].specified).toBe(true);
+			expect(element.attributes['key2'].ownerElement === element).toBe(true);
+			expect(element.attributes['key2'].ownerDocument === document).toBe(true);
+
+			expect(element.attributes['key3'].name).toBe('key3');
+			expect(element.attributes['key3'].value).toBe('value3');
+			expect(element.attributes['key3'].namespaceURI).toBe(null);
+			expect(element.attributes['key3'].specified).toBe(true);
+			expect(element.attributes['key3'].ownerElement === element).toBe(true);
+			expect(element.attributes['key3'].ownerDocument === document).toBe(true);
 		});
 	});
 
@@ -1120,19 +1120,19 @@ describe('Element', () => {
 			expect(element.attributes[1].ownerElement === element).toBe(true);
 			expect(element.attributes[1].ownerDocument === document).toBe(true);
 
-			expect(element.attributes.key1.name).toBe('key1');
-			expect(element.attributes.key1.value).toBe('value1');
-			expect(element.attributes.key1.namespaceURI).toBe(null);
-			expect(element.attributes.key1.specified).toBe(true);
-			expect(element.attributes.key1.ownerElement === element).toBe(true);
-			expect(element.attributes.key1.ownerDocument === document).toBe(true);
+			expect(element.attributes['key1'].name).toBe('key1');
+			expect(element.attributes['key1'].value).toBe('value1');
+			expect(element.attributes['key1'].namespaceURI).toBe(null);
+			expect(element.attributes['key1'].specified).toBe(true);
+			expect(element.attributes['key1'].ownerElement === element).toBe(true);
+			expect(element.attributes['key1'].ownerDocument === document).toBe(true);
 
-			expect(element.attributes.key2.name).toBe('key2');
-			expect(element.attributes.key2.value).toBe('');
-			expect(element.attributes.key2.namespaceURI).toBe(null);
-			expect(element.attributes.key2.specified).toBe(true);
-			expect(element.attributes.key2.ownerElement === element).toBe(true);
-			expect(element.attributes.key2.ownerDocument === document).toBe(true);
+			expect(element.attributes['key2'].name).toBe('key2');
+			expect(element.attributes['key2'].value).toBe('');
+			expect(element.attributes['key2'].namespaceURI).toBe(null);
+			expect(element.attributes['key2'].specified).toBe(true);
+			expect(element.attributes['key2'].ownerElement === element).toBe(true);
+			expect(element.attributes['key2'].ownerDocument === document).toBe(true);
 		});
 	});
 
@@ -1212,9 +1212,7 @@ describe('Element', () => {
 		it('Removes an attribute.', () => {
 			element.setAttribute('key1', 'value1');
 			element.removeAttribute('key1');
-			expect(element.attributes).toEqual({
-				length: 0
-			});
+			expect(element.attributes.length).toBe(0);
 		});
 	});
 
@@ -1222,9 +1220,7 @@ describe('Element', () => {
 		it('Removes a namespace attribute.', () => {
 			element.setAttributeNS(NAMESPACE_URI, 'global:local', 'value');
 			element.removeAttributeNS(NAMESPACE_URI, 'local');
-			expect(element.attributes).toEqual({
-				length: 0
-			});
+			expect(element.attributes.length).toBe(0);
 		});
 	});
 
@@ -1363,19 +1359,19 @@ describe('Element', () => {
 				expect((<IAttr>element.attributes[1]).ownerElement).toBe(element);
 				expect((<IAttr>element.attributes[1]).ownerDocument).toBe(document);
 
-				expect((<IAttr>element.attributes.key1).name).toBe('key1');
-				expect((<IAttr>element.attributes.key1).namespaceURI).toBe(NamespaceURI.svg);
-				expect((<IAttr>element.attributes.key1).value).toBe('value1');
-				expect((<IAttr>element.attributes.key1).specified).toBe(true);
-				expect((<IAttr>element.attributes.key1).ownerElement).toBe(element);
-				expect((<IAttr>element.attributes.key1).ownerDocument).toBe(document);
-
-				expect((<IAttr>element.attributes.key2).name).toBe('key2');
-				expect((<IAttr>element.attributes.key2).namespaceURI).toBe(null);
-				expect((<IAttr>element.attributes.key2).value).toBe('value2');
-				expect((<IAttr>element.attributes.key2).specified).toBe(true);
-				expect((<IAttr>element.attributes.key2).ownerElement).toBe(element);
-				expect((<IAttr>element.attributes.key2).ownerDocument).toBe(document);
+				expect((<IAttr>element.attributes['key1']).name).toBe('key1');
+				expect((<IAttr>element.attributes['key1']).namespaceURI).toBe(NamespaceURI.svg);
+				expect((<IAttr>element.attributes['key1']).value).toBe('value1');
+				expect((<IAttr>element.attributes['key1']).specified).toBe(true);
+				expect((<IAttr>element.attributes['key1']).ownerElement).toBe(element);
+				expect((<IAttr>element.attributes['key1']).ownerDocument).toBe(document);
+
+				expect((<IAttr>element.attributes['key2']).name).toBe('key2');
+				expect((<IAttr>element.attributes['key2']).namespaceURI).toBe(null);
+				expect((<IAttr>element.attributes['key2']).value).toBe('value2');
+				expect((<IAttr>element.attributes['key2']).specified).toBe(true);
+				expect((<IAttr>element.attributes['key2']).ownerElement).toBe(element);
+				expect((<IAttr>element.attributes['key2']).ownerDocument).toBe(document);
 			});
 
 			it('Sets an Attr node on an <svg> element.', () => {
@@ -1405,19 +1401,19 @@ describe('Element', () => {
 				expect((<IAttr>svg.attributes[1]).ownerElement).toBe(svg);
 				expect((<IAttr>svg.attributes[1]).ownerDocument).toBe(document);
 
-				expect((<IAttr>svg.attributes.KEY1).name).toBe('KEY1');
-				expect((<IAttr>svg.attributes.KEY1).namespaceURI).toBe(NamespaceURI.svg);
-				expect((<IAttr>svg.attributes.KEY1).value).toBe('value1');
-				expect((<IAttr>svg.attributes.KEY1).specified).toBe(true);
-				expect((<IAttr>svg.attributes.KEY1).ownerElement).toBe(svg);
-				expect((<IAttr>svg.attributes.KEY1).ownerDocument).toBe(document);
-
-				expect((<IAttr>svg.attributes.key2).name).toBe('key2');
-				expect((<IAttr>svg.attributes.key2).namespaceURI).toBe(null);
-				expect((<IAttr>svg.attributes.key2).value).toBe('value2');
-				expect((<IAttr>svg.attributes.key2).specified).toBe(true);
-				expect((<IAttr>svg.attributes.key2).ownerElement).toBe(svg);
-				expect((<IAttr>svg.attributes.key2).ownerDocument).toBe(document);
+				expect((<IAttr>svg.attributes['KEY1']).name).toBe('KEY1');
+				expect((<IAttr>svg.attributes['KEY1']).namespaceURI).toBe(NamespaceURI.svg);
+				expect((<IAttr>svg.attributes['KEY1']).value).toBe('value1');
+				expect((<IAttr>svg.attributes['KEY1']).specified).toBe(true);
+				expect((<IAttr>svg.attributes['KEY1']).ownerElement).toBe(svg);
+				expect((<IAttr>svg.attributes['KEY1']).ownerDocument).toBe(document);
+
+				expect((<IAttr>svg.attributes['key2']).name).toBe('key2');
+				expect((<IAttr>svg.attributes['key2']).namespaceURI).toBe(null);
+				expect((<IAttr>svg.attributes['key2']).value).toBe('value2');
+				expect((<IAttr>svg.attributes['key2']).specified).toBe(true);
+				expect((<IAttr>svg.attributes['key2']).ownerElement).toBe(svg);
+				expect((<IAttr>svg.attributes['key2']).ownerDocument).toBe(document);
 			});
 		});
 	}
@@ -1492,7 +1488,7 @@ describe('Element', () => {
 				element.setAttributeNode(attribute);
 				element[method](attribute);
 
-				expect(element.attributes).toEqual({ length: 0 });
+				expect(element.attributes.length).toBe(0);
 			});
 		});
 	}
diff --git a/packages/happy-dom/test/xml-parser/XMLParser.test.ts b/packages/happy-dom/test/xml-parser/XMLParser.test.ts
index 17d21380..e1448b65 100644
--- a/packages/happy-dom/test/xml-parser/XMLParser.test.ts
+++ b/packages/happy-dom/test/xml-parser/XMLParser.test.ts
@@ -80,25 +80,25 @@ describe('XMLParser', () => {
 				true
 			);
 
-			expect((<IHTMLElement>root.childNodes[0]).attributes.class.name).toBe('class');
-			expect((<IHTMLElement>root.childNodes[0]).attributes.class.value).toBe('class1 class2');
-			expect((<IHTMLElement>root.childNodes[0]).attributes.class.namespaceURI).toBe(null);
-			expect((<IHTMLElement>root.childNodes[0]).attributes.class.specified).toBe(true);
+			expect((<IHTMLElement>root.childNodes[0]).attributes['class'].name).toBe('class');
+			expect((<IHTMLElement>root.childNodes[0]).attributes['class'].value).toBe('class1 class2');
+			expect((<IHTMLElement>root.childNodes[0]).attributes['class'].namespaceURI).toBe(null);
+			expect((<IHTMLElement>root.childNodes[0]).attributes['class'].specified).toBe(true);
 			expect(
-				(<IHTMLElement>root.childNodes[0]).attributes.class.ownerElement === root.childNodes[0]
+				(<IHTMLElement>root.childNodes[0]).attributes['class'].ownerElement === root.childNodes[0]
+			).toBe(true);
+			expect(
+				(<IHTMLElement>root.childNodes[0]).attributes['class'].ownerDocument === document
 			).toBe(true);
-			expect((<IHTMLElement>root.childNodes[0]).attributes.class.ownerDocument === document).toBe(
-				true
-			);
 
-			expect((<IHTMLElement>root.childNodes[0]).attributes.id.name).toBe('id');
-			expect((<IHTMLElement>root.childNodes[0]).attributes.id.value).toBe('id');
-			expect((<IHTMLElement>root.childNodes[0]).attributes.id.namespaceURI).toBe(null);
-			expect((<IHTMLElement>root.childNodes[0]).attributes.id.specified).toBe(true);
+			expect((<IHTMLElement>root.childNodes[0]).attributes['id'].name).toBe('id');
+			expect((<IHTMLElement>root.childNodes[0]).attributes['id'].value).toBe('id');
+			expect((<IHTMLElement>root.childNodes[0]).attributes['id'].namespaceURI).toBe(null);
+			expect((<IHTMLElement>root.childNodes[0]).attributes['id'].specified).toBe(true);
 			expect(
-				(<IHTMLElement>root.childNodes[0]).attributes.id.ownerElement === root.childNodes[0]
+				(<IHTMLElement>root.childNodes[0]).attributes['id'].ownerElement === root.childNodes[0]
 			).toBe(true);
-			expect((<IHTMLElement>root.childNodes[0]).attributes.id.ownerDocument === document).toBe(
+			expect((<IHTMLElement>root.childNodes[0]).attributes['id'].ownerDocument === document).toBe(
 				true
 			);
 
@@ -295,33 +295,33 @@ describe('XMLParser', () => {
 			expect(svg.attributes[3].ownerElement === svg).toBe(true);
 			expect(svg.attributes[3].ownerDocument === document).toBe(true);
 
-			expect(svg.attributes.viewBox.name).toBe('viewBox');
-			expect(svg.attributes.viewBox.value).toBe('0 0 300 100');
-			expect(svg.attributes.viewBox.namespaceURI).toBe(null);
-			expect(svg.attributes.viewBox.specified).toBe(true);
-			expect(svg.attributes.viewBox.ownerElement === svg).toBe(true);
-			expect(svg.attributes.viewBox.ownerDocument === document).toBe(true);
-
-			expect(svg.attributes.stroke.name).toBe('stroke');
-			expect(svg.attributes.stroke.value).toBe('red');
-			expect(svg.attributes.stroke.namespaceURI).toBe(null);
-			expect(svg.attributes.stroke.specified).toBe(true);
-			expect(svg.attributes.stroke.ownerElement === svg).toBe(true);
-			expect(svg.attributes.stroke.ownerDocument === document).toBe(true);
-
-			expect(svg.attributes.fill.name).toBe('fill');
-			expect(svg.attributes.fill.value).toBe('grey');
-			expect(svg.attributes.fill.namespaceURI).toBe(null);
-			expect(svg.attributes.fill.specified).toBe(true);
-			expect(svg.attributes.fill.ownerElement === svg).toBe(true);
-			expect(svg.attributes.fill.ownerDocument === document).toBe(true);
-
-			expect(svg.attributes.xmlns.name).toBe('xmlns');
-			expect(svg.attributes.xmlns.value).toBe(NamespaceURI.html);
-			expect(svg.attributes.xmlns.namespaceURI).toBe(NamespaceURI.html);
-			expect(svg.attributes.xmlns.specified).toBe(true);
-			expect(svg.attributes.xmlns.ownerElement === svg).toBe(true);
-			expect(svg.attributes.xmlns.ownerDocument === document).toBe(true);
+			expect(svg.attributes['viewBox'].name).toBe('viewBox');
+			expect(svg.attributes['viewBox'].value).toBe('0 0 300 100');
+			expect(svg.attributes['viewBox'].namespaceURI).toBe(null);
+			expect(svg.attributes['viewBox'].specified).toBe(true);
+			expect(svg.attributes['viewBox'].ownerElement === svg).toBe(true);
+			expect(svg.attributes['viewBox'].ownerDocument === document).toBe(true);
+
+			expect(svg.attributes['stroke'].name).toBe('stroke');
+			expect(svg.attributes['stroke'].value).toBe('red');
+			expect(svg.attributes['stroke'].namespaceURI).toBe(null);
+			expect(svg.attributes['stroke'].specified).toBe(true);
+			expect(svg.attributes['stroke'].ownerElement === svg).toBe(true);
+			expect(svg.attributes['stroke'].ownerDocument === document).toBe(true);
+
+			expect(svg.attributes['fill'].name).toBe('fill');
+			expect(svg.attributes['fill'].value).toBe('grey');
+			expect(svg.attributes['fill'].namespaceURI).toBe(null);
+			expect(svg.attributes['fill'].specified).toBe(true);
+			expect(svg.attributes['fill'].ownerElement === svg).toBe(true);
+			expect(svg.attributes['fill'].ownerDocument === document).toBe(true);
+
+			expect(svg.attributes['xmlns'].name).toBe('xmlns');
+			expect(svg.attributes['xmlns'].value).toBe(NamespaceURI.html);
+			expect(svg.attributes['xmlns'].namespaceURI).toBe(NamespaceURI.html);
+			expect(svg.attributes['xmlns'].specified).toBe(true);
+			expect(svg.attributes['xmlns'].ownerElement === svg).toBe(true);
+			expect(svg.attributes['xmlns'].ownerDocument === document).toBe(true);
 
 			expect(new XMLSerializer().serializeToString(root).replace(/[\s]/gm, '')).toBe(
 				`