From f9c9ee927e4ac0de1e07f0c6bcc8f0499c860d7d Mon Sep 17 00:00:00 2001 From: David Ortner Date: Wed, 2 Aug 2023 14:29:55 +0200 Subject: [PATCH] #728@patch: Improves support for NamedNodeMap, which is used as the Element.attributes property. It will now reflect any changes done to it on the Element itself. --- .../happy-dom/bin/change-file-extension.cjs | 1 - .../AbstractCSSStyleDeclaration.ts | 52 +++-- .../CSSStyleDeclarationElementStyle.ts | 7 +- .../src/named-node-map/INamedNodeMap.ts | 52 ++--- .../src/named-node-map/NamedNodeMap.ts | 208 ++++++++++++------ .../happy-dom/src/nodes/element/Dataset.ts | 36 +-- .../happy-dom/src/nodes/element/Element.ts | 203 +++-------------- .../src/nodes/element/ElementNamedNodeMap.ts | 194 ++++++++++++++++ .../src/nodes/element/ElementUtility.ts | 38 ++-- .../happy-dom/src/nodes/element/IElement.ts | 10 +- .../html-anchor-element/HTMLAnchorElement.ts | 38 +--- .../HTMLAnchorElementNamedNodeMap.ts | 48 ++++ .../html-button-element/HTMLButtonElement.ts | 36 +-- .../HTMLButtonElementNamedNodeMap.ts | 56 +++++ .../src/nodes/html-element/HTMLElement.ts | 32 +-- .../html-element/HTMLElementNamedNodeMap.ts | 38 ++++ .../html-iframe-element/HTMLIFrameElement.ts | 22 +- .../HTMLIFrameElementNamedNodeMap.ts | 26 +++ .../html-input-element/HTMLInputElement.ts | 37 +--- .../HTMLInputElementNamedNodeMap.ts | 57 +++++ .../html-link-element/HTMLLinkElement.ts | 36 +-- .../HTMLLinkElementNamedNodeMap.ts | 43 ++++ .../html-option-element/HTMLOptionElement.ts | 46 +--- .../HTMLOptionElementNamedNodeMap.ts | 55 +++++ .../html-script-element/HTMLScriptElement.ts | 21 +- .../HTMLScriptElementNamedNodeMap.ts | 26 +++ .../html-select-element/HTMLSelectElement.ts | 37 +--- .../HTMLSelectElementNamedNodeMap.ts | 57 +++++ .../HTMLTextAreaElement.ts | 36 +-- .../HTMLTextAreaElementNamedNodeMap.ts | 57 +++++ .../HTMLUnknownElement.ts | 8 +- .../happy-dom/src/nodes/node/NodeUtility.ts | 31 +-- .../src/nodes/svg-element/SVGElement.ts | 33 +-- .../svg-element/SVGElementNamedNodeMap.ts | 38 ++++ .../src/query-selector/SelectorItem.ts | 2 +- .../src/xml-serializer/XMLSerializer.ts | 6 +- .../test/named-node-map/NamedNodeMap.test.ts | 44 ++-- .../HTMLUnknownElement.test.ts | 8 +- 38 files changed, 1053 insertions(+), 722 deletions(-) create mode 100644 packages/happy-dom/src/nodes/element/ElementNamedNodeMap.ts create mode 100644 packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElementNamedNodeMap.ts create mode 100644 packages/happy-dom/src/nodes/html-button-element/HTMLButtonElementNamedNodeMap.ts create mode 100644 packages/happy-dom/src/nodes/html-element/HTMLElementNamedNodeMap.ts create mode 100644 packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementNamedNodeMap.ts create mode 100644 packages/happy-dom/src/nodes/html-input-element/HTMLInputElementNamedNodeMap.ts create mode 100644 packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementNamedNodeMap.ts create mode 100644 packages/happy-dom/src/nodes/html-option-element/HTMLOptionElementNamedNodeMap.ts create mode 100644 packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementNamedNodeMap.ts create mode 100644 packages/happy-dom/src/nodes/html-select-element/HTMLSelectElementNamedNodeMap.ts create mode 100644 packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElementNamedNodeMap.ts create mode 100644 packages/happy-dom/src/nodes/svg-element/SVGElementNamedNodeMap.ts diff --git a/packages/happy-dom/bin/change-file-extension.cjs b/packages/happy-dom/bin/change-file-extension.cjs index f63a56eff..8000b421a 100644 --- a/packages/happy-dom/bin/change-file-extension.cjs +++ b/packages/happy-dom/bin/change-file-extension.cjs @@ -64,7 +64,6 @@ async function renameFiles(files, args) { for (const file of newFiles) { writePromises.push( FS.promises.readFile(file.oldPath).then((content) => { - debugger; return FS.promises .writeFile( file.newPath, diff --git a/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts b/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts index 36edc8aa9..227b0ac8e 100644 --- a/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts +++ b/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts @@ -1,10 +1,11 @@ import IElement from '../../nodes/element/IElement.js'; -import Attr from '../../nodes/attr/Attr.js'; +import IAttr from '../../nodes/attr/IAttr.js'; import CSSRule from '../CSSRule.js'; import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; import DOMException from '../../exception/DOMException.js'; import CSSStyleDeclarationElementStyle from './element-style/CSSStyleDeclarationElementStyle.js'; import CSSStyleDeclarationPropertyManager from './property-manager/CSSStyleDeclarationPropertyManager.js'; +import NamedNodeMap from '../../named-node-map/NamedNodeMap.js'; /** * CSS Style Declaration. @@ -77,17 +78,21 @@ export default abstract class AbstractCSSStyleDeclaration { if (this._ownerElement) { const style = new CSSStyleDeclarationPropertyManager({ cssText }); - if (!this._ownerElement['_attributes']['style']) { - Attr._ownerDocument = this._ownerElement.ownerDocument; - this._ownerElement['_attributes']['style'] = new Attr(); - this._ownerElement['_attributes']['style'].name = 'style'; + let styleAttribute = this._ownerElement.attributes['style']; + + if (!styleAttribute) { + styleAttribute = this._ownerElement.ownerDocument.createAttribute('style'); + // We use "_setNamedItemWithoutConsequences" here to avoid triggering setting "Element.style.cssText" when setting the "style" attribute. + (this._ownerElement.attributes)._setNamedItemWithoutConsequences( + styleAttribute + ); } if (this._ownerElement.isConnected) { this._ownerElement.ownerDocument['_cacheID']++; } - this._ownerElement['_attributes']['style'].value = style.toString(); + styleAttribute.value = style.toString(); } else { this._style = new CSSStyleDeclarationPropertyManager({ cssText }); } @@ -130,20 +135,25 @@ export default abstract class AbstractCSSStyleDeclaration { if (!stringValue) { this.removeProperty(name); } else if (this._ownerElement) { - if (!this._ownerElement['_attributes']['style']) { - Attr._ownerDocument = this._ownerElement.ownerDocument; - this._ownerElement['_attributes']['style'] = new Attr(); - this._ownerElement['_attributes']['style'].name = 'style'; - } + let styleAttribute = this._ownerElement.attributes['style']; - const style = this._elementStyle.getElementStyle(); - style.set(name, stringValue, !!priority); + if (!styleAttribute) { + styleAttribute = this._ownerElement.ownerDocument.createAttribute('style'); + + // We use "_setNamedItemWithoutConsequences" here to avoid triggering setting "Element.style.cssText" when setting the "style" attribute. + (this._ownerElement.attributes)._setNamedItemWithoutConsequences( + styleAttribute + ); + } if (this._ownerElement.isConnected) { this._ownerElement.ownerDocument['_cacheID']++; } - this._ownerElement['_attributes']['style'].value = style.toString(); + const style = this._elementStyle.getElementStyle(); + style.set(name, stringValue, !!priority); + + styleAttribute.value = style.toString(); } else { this._style.set(name, stringValue, !!priority); } @@ -168,14 +178,16 @@ export default abstract class AbstractCSSStyleDeclaration { const style = this._elementStyle.getElementStyle(); style.remove(name); const newCSSText = style.toString(); - if (newCSSText) { - if (this._ownerElement.isConnected) { - this._ownerElement.ownerDocument['_cacheID']++; - } - this._ownerElement['_attributes']['style'].value = newCSSText; + if (this._ownerElement.isConnected) { + this._ownerElement.ownerDocument['_cacheID']++; + } + + if (newCSSText) { + (this._ownerElement.attributes['style']).value = newCSSText; } else { - delete this._ownerElement['_attributes']['style']; + // We use "_removeNamedItemWithoutConsequences" here to avoid triggering setting "Element.style.cssText" when setting the "style" attribute. + (this._ownerElement.attributes)._removeNamedItemWithoutConsequences('style'); } } else { this._style.remove(name); diff --git a/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts b/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts index 8be81ce83..f0f7df91a 100644 --- a/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts +++ b/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts @@ -63,7 +63,7 @@ export default class CSSStyleDeclarationElementStyle { return this.getComputedElementStyle(); } - const cssText = this.element['_attributes']['style']?.value; + const cssText = this.element.attributes['style']?.value; if (cssText) { if (this.cache.propertyManager && this.cache.cssText === cssText) { @@ -182,8 +182,9 @@ export default class CSSStyleDeclarationElementStyle { elementCSSText += cssText.cssText; } - if (parentElement.element['_attributes']['style']?.value) { - elementCSSText += parentElement.element['_attributes']['style'].value; + const elementStyleAttribute = (parentElement.element).attributes['style']; + if (elementStyleAttribute) { + elementCSSText += elementStyleAttribute.value; } CSSStyleDeclarationCSSParser.parse(elementCSSText, (name, value, important) => { diff --git a/packages/happy-dom/src/named-node-map/INamedNodeMap.ts b/packages/happy-dom/src/named-node-map/INamedNodeMap.ts index 868531948..1cf23829b 100644 --- a/packages/happy-dom/src/named-node-map/INamedNodeMap.ts +++ b/packages/happy-dom/src/named-node-map/INamedNodeMap.ts @@ -6,65 +6,65 @@ import IAttr from '../nodes/attr/IAttr.js'; * Reference: * https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap. */ -export default interface INamedNodeMap extends Iterable { +export default interface INamedNodeMap { [index: number]: IAttr; [Symbol.toStringTag]: string; readonly length: number; /** - * Returns attribute by index. + * Returns item by index. * * @param index Index. */ - item: (index: number) => IAttr; + item: (index: number) => IAttr | null; /** - * Returns attribute by name. + * Returns named item. * - * @param qualifiedName Name. - * @returns Attribute. + * @param name Name. + * @returns Itme. */ - getNamedItem: (qualifiedName: string) => IAttr; + getNamedItem(name: string): IAttr | null; /** - * Returns attribute by name and namespace. + * Returns item by name and namespace. * * @param namespace Namespace. * @param localName Local name of the attribute. - * @returns Attribute. + * @returns Item. */ - getNamedItemNS: (namespace: string, localName: string) => IAttr; + getNamedItemNS(namespace: string, localName: string): IAttr | null; /** - * Adds a new attribute node. + * Sets named item. * - * @param attr Attribute. - * @returns Replaced attribute. + * @param item Item. + * @returns Replaced item. */ - setNamedItem: (attr: IAttr) => IAttr; + setNamedItem(item: IAttr): IAttr | null; /** - * Adds a new namespaced attribute node. + * Adds a new namespaced item. * - * @param attr Attribute. - * @returns Replaced attribute. + * @param item Item. + * @returns Replaced item. */ - setNamedItemNS: (attr: IAttr) => IAttr; + setNamedItemNS(item: IAttr): IAttr | null; /** - * Removes an attribute. + * Removes an item. * - * @param qualifiedName Name of the attribute. - * @returns Removed attribute. + * @param name Name of item. + * @returns Removed item. */ - removeNamedItem: (qualifiedName: string) => IAttr; + removeNamedItem(name: string): IAttr | null; /** - * Removes a namespaced attribute. + * Removes a namespaced item. * * @param namespace Namespace. - * @param localName Local name of the attribute. - * @returns Removed attribute. + * @param localName Local name of the item. + * @returns Removed item. */ - removeNamedItemNS: (namespace: string, localName: string) => IAttr; + removeNamedItemNS(namespace: string, localName: string): IAttr | null; } diff --git a/packages/happy-dom/src/named-node-map/NamedNodeMap.ts b/packages/happy-dom/src/named-node-map/NamedNodeMap.ts index d2c74664a..3236f1139 100644 --- a/packages/happy-dom/src/named-node-map/NamedNodeMap.ts +++ b/packages/happy-dom/src/named-node-map/NamedNodeMap.ts @@ -1,145 +1,207 @@ -import type Element from '../nodes/element/Element.js'; -import IAttr from '../nodes/attr/IAttr.js'; import INamedNodeMap from './INamedNodeMap.js'; +import IAttr from '../nodes/attr/IAttr.js'; /** - * NamedNodeMap. + * Named Node Map. * - * Reference: - * https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap. + * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap */ export default class NamedNodeMap implements INamedNodeMap { [index: number]: IAttr; + public length = 0; + protected _namedItems: { [k: string]: IAttr } = {}; /** - * Reference to the element. + * Returns string. + * + * @returns string. */ - #ownerElement: Element; + public get [Symbol.toStringTag](): string { + return 'NamedNodeMap'; + } /** - * Constructor. + * Iterator. * - * @param element Associated element. + * @returns Iterator. */ - constructor(element: Element) { - this.#ownerElement = element; + public *[Symbol.iterator](): IterableIterator { + for (let i = 0, max = this.length; i < max; i++) { + yield this[i]; + } } /** - * Returns string. + * Returns item by index. * - * @returns string. + * @param index Index. */ - public get [Symbol.toStringTag](): string { - return this.constructor.name; + public item(index: number): IAttr | null { + return index >= 0 && this[index] ? this[index] : null; } /** - * Length. + * Returns named item. * - * @returns Length. + * @param name Name. + * @returns Itme. */ - public get length(): number { - return Object.keys(this.#ownerElement._attributes).length; + public getNamedItem(name: string): IAttr | null { + return this._namedItems[name] || null; } /** - * Returns attribute by index. + * Returns item by name and namespace. * - * @param index Index. + * @param namespace Namespace. + * @param localName Local name of the attribute. + * @returns Item. */ - public item(index: number): IAttr | null { - if (index < 0) { - return null; + public getNamedItemNS(namespace: string, localName: string): IAttr | null { + const attribute = this.getNamedItem(localName); + + if (attribute && attribute.namespaceURI === namespace && attribute.localName === localName) { + return attribute; + } + + for (let i = 0, max = this.length; i < max; i++) { + if (this[i].namespaceURI === namespace && this[i].localName === localName) { + return this[i]; + } } - const attr = Object.values(this.#ownerElement._attributes)[index]; - return attr ? attr : null; + + return null; } /** - * Returns attribute by name. + * Sets named item. * - * @param qualifiedName Name. - * @returns Attribute. + * @param item Item. + * @returns Replaced item. */ - public getNamedItem(qualifiedName: string): IAttr | null { - return this.#ownerElement.getAttributeNode(qualifiedName); + public setNamedItem(item: IAttr): IAttr | null { + return this._setNamedItemWithoutConsequences(item); } /** - * Returns attribute by name and namespace. + * Adds a new namespaced item. * - * @param namespace Namespace. - * @param localName Local name of the attribute. - * @returns Attribute. + * @alias setNamedItem() + * @param item Item. + * @returns Replaced item. */ - public getNamedItemNS(namespace: string, localName: string): IAttr | null { - return this.#ownerElement.getAttributeNodeNS(namespace, localName); + public setNamedItemNS(item: IAttr): IAttr | null { + return this.setNamedItem(item); } /** - * Adds a new attribute node. + * Removes an item. * - * @param attr Attribute. - * @returns Replaced attribute. + * @param name Name of item. + * @returns Removed item. */ - public setNamedItem(attr: IAttr): IAttr { - return this.#ownerElement.setAttributeNode(attr); + public removeNamedItem(name: string): IAttr | null { + return this._removeNamedItemWithoutConsequences(name); } /** - * Adds a new namespaced attribute node. + * Removes a namespaced item. * - * @param attr Attribute. - * @returns Replaced attribute. + * @param namespace Namespace. + * @param localName Local name of the item. + * @returns Removed item. */ - public setNamedItemNS(attr: IAttr): IAttr { - return this.#ownerElement.setAttributeNodeNS(attr); + public removeNamedItemNS(namespace: string, localName: string): IAttr | null { + const attribute = this.getNamedItemNS(namespace, localName); + if (attribute) { + return this.removeNamedItem(attribute.name); + } + return null; } /** - * Removes an attribute. + * Sets named item without calling listeners for certain attributes. * - * @param qualifiedName Name of the attribute. - * @returns Removed attribute. + * @param item Item. + * @returns Replaced item. */ - public removeNamedItem(qualifiedName: string): IAttr | null { - const attr = this.getNamedItem(qualifiedName); + public _setNamedItemWithoutConsequences(item: IAttr): IAttr | null { + if (item.name) { + const replacedItem = this._namedItems[item.name] || null; + + this._namedItems[item.name] = item; + + if (replacedItem) { + this._removeNamedItemIndex(replacedItem); + } - if (attr) { - this.#ownerElement.removeAttributeNode(attr); + this[this.length] = item; + this.length++; + + if (this._isValidPropertyName(item.name)) { + this[item.name] = item; + } + + return replacedItem; } - return attr; + return null; } /** - * Removes a namespaced attribute. + * Removes an item without calling listeners for certain attributes. * - * @param namespace Namespace. - * @param localName Local name of the attribute. - * @returns Removed attribute. + * @param name Name of item. + * @returns Removed item. */ - public removeNamedItemNS(namespace: string, localName: string): IAttr | null { - const attr = this.getNamedItemNS(namespace, localName); + public _removeNamedItemWithoutConsequences(name: string): IAttr | null { + const removedItem = this._namedItems[name] || null; - if (attr) { - this.#ownerElement.removeAttributeNode(attr); + if (!removedItem) { + return null; } - return attr; + + this._removeNamedItemIndex(removedItem); + + if (this[name] === removedItem) { + delete this[name]; + } + + delete this._namedItems[name]; + + return removedItem; } /** - * Iterator. + * Removes an item from index. * - * @returns Iterator. + * @param item Item. */ - public [Symbol.iterator](): Iterator { - let index = -1; - return { - next: () => { - index++; - return { value: this.item(index), done: index >= this.length }; + protected _removeNamedItemIndex(item: IAttr): void { + for (let i = 0; i < this.length; i++) { + if (this[i] === item) { + for (let b = i; b < this.length; b++) { + if (b < this.length - 1) { + this[b] = this[b + 1]; + } else { + delete this[b]; + } + } + this.length--; + break; } - }; + } + } + + /** + * Returns "true" if the property name is valid. + * + * @param name Name. + * @returns True if the property name is valid. + */ + protected _isValidPropertyName(name: string): boolean { + return ( + !this.constructor.prototype.hasOwnProperty(name) && + (isNaN(Number(name)) || name.includes('.')) + ); } } diff --git a/packages/happy-dom/src/nodes/element/Dataset.ts b/packages/happy-dom/src/nodes/element/Dataset.ts index 914b7118e..29dff1122 100644 --- a/packages/happy-dom/src/nodes/element/Dataset.ts +++ b/packages/happy-dom/src/nodes/element/Dataset.ts @@ -20,11 +20,12 @@ export default class Dataset { constructor(element: Element) { // Build the initial dataset record from all data attributes. const dataset: DatasetRecord = {}; - const attributes = element._attributes; - for (const name of Object.keys(attributes)) { - if (name.startsWith('data-')) { - const key = Dataset.kebabToCamelCase(name.replace('data-', '')); - dataset[key] = attributes[name].value; + + for (let i = 0, max = element.attributes.length; i < max; i++) { + const attribute = element.attributes[i]; + if (attribute.name.startsWith('data-')) { + const key = Dataset.kebabToCamelCase(attribute.name.replace('data-', '')); + dataset[key] = attribute.value; } } @@ -32,9 +33,9 @@ export default class Dataset { // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy this.proxy = new Proxy(dataset, { get(dataset: DatasetRecord, key: string): string { - const name = 'data-' + Dataset.camelCaseToKebab(key); - if (name in attributes) { - return (dataset[key] = attributes[name].value); + const attribute = element.attributes.getNamedItem('data-' + Dataset.camelCaseToKebab(key)); + if (attribute) { + return (dataset[key] = attribute.value); } delete dataset[key]; return undefined; @@ -45,10 +46,10 @@ export default class Dataset { return true; }, deleteProperty(dataset: DatasetRecord, key: string): boolean { - const name = 'data-' + Dataset.camelCaseToKebab(key); - const result1 = delete attributes[name]; - const result2 = delete dataset[key]; - return result1 && result2; + return ( + !!element.attributes.removeNamedItem('data-' + Dataset.camelCaseToKebab(key)) && + delete dataset[key] + ); }, ownKeys(dataset: DatasetRecord): string[] { // According to Mozilla we have to update the dataset object (target) to contain the same keys as what we return: @@ -56,11 +57,12 @@ export default class Dataset { // "The result List must contain the keys of all non-configurable own properties of the target object." const keys = []; const deleteKeys = []; - for (const name of Object.keys(attributes)) { - if (name.startsWith('data-')) { - const key = Dataset.kebabToCamelCase(name.replace('data-', '')); + for (let i = 0, max = element.attributes.length; i < max; i++) { + const attribute = element.attributes[i]; + if (attribute.name.startsWith('data-')) { + const key = Dataset.kebabToCamelCase(attribute.name.replace('data-', '')); keys.push(key); - dataset[key] = attributes[name].value; + dataset[key] = attribute.value; if (!dataset[key]) { deleteKeys.push(key); } @@ -72,7 +74,7 @@ export default class Dataset { return keys; }, has(_dataset: DatasetRecord, key: string): boolean { - return !!attributes['data-' + Dataset.camelCaseToKebab(key)]; + return !!element.attributes.getNamedItem('data-' + Dataset.camelCaseToKebab(key)); } }); } diff --git a/packages/happy-dom/src/nodes/element/Element.ts b/packages/happy-dom/src/nodes/element/Element.ts index 8fcaa92be..be8358c0c 100644 --- a/packages/happy-dom/src/nodes/element/Element.ts +++ b/packages/happy-dom/src/nodes/element/Element.ts @@ -1,13 +1,10 @@ import Node from '../node/Node.js'; import ShadowRoot from '../shadow-root/ShadowRoot.js'; import Attr from '../attr/Attr.js'; -import NamedNodeMap from '../../named-node-map/NamedNodeMap.js'; import DOMRect from './DOMRect.js'; import DOMTokenList from '../../dom-token-list/DOMTokenList.js'; import IDOMTokenList from '../../dom-token-list/IDOMTokenList.js'; import QuerySelector from '../../query-selector/QuerySelector.js'; -import MutationRecord from '../../mutation-observer/MutationRecord.js'; -import MutationTypeEnum from '../../mutation-observer/MutationTypeEnum.js'; import NamespaceURI from '../../config/NamespaceURI.js'; import XMLParser from '../../xml-parser/XMLParser.js'; import XMLSerializer from '../../xml-serializer/XMLSerializer.js'; @@ -32,6 +29,7 @@ import ElementUtility from './ElementUtility.js'; import HTMLCollection from './HTMLCollection.js'; import EventPhaseEnum from '../../event/EventPhaseEnum.js'; import CSSStyleDeclaration from '../../css/declaration/CSSStyleDeclaration.js'; +import ElementNamedNodeMap from './ElementNamedNodeMap.js'; /** * Element. @@ -89,9 +87,9 @@ export default class Element extends Node implements IElement { // Used for being able to access closed shadow roots public _shadowRoot: IShadowRoot = null; - public _attributes: { [k: string]: IAttr } = {}; + public readonly attributes: INamedNodeMap = new ElementNamedNodeMap(this); - private _classList: DOMTokenList = null; + public _classList: DOMTokenList = null; public _isValue?: string | null = null; public _computedStyle: CSSStyleDeclaration | null = null; @@ -266,15 +264,6 @@ export default class Element extends Node implements IElement { this.replaceWith(html); } - /** - * Returns attributes. - * - * @returns Attributes. - */ - public get attributes(): INamedNodeMap { - return Object.assign(new NamedNodeMap(this), Object.values(this._attributes), this._attributes); - } - /** * First element child. * @@ -364,10 +353,9 @@ export default class Element extends Node implements IElement { Attr._ownerDocument = this.ownerDocument; - for (const key of Object.keys(this._attributes)) { - const attr = Object.assign(new Attr(), this._attributes[key]); - (attr.ownerElement) = clone; - (clone)._attributes[key] = attr; + for (let i = 0, max = this.attributes.length; i < max; i++) { + const attribute = this.attributes[i]; + clone.attributes.setNamedItem(Object.assign(new Attr(), attribute)); } if (deep) { @@ -563,7 +551,11 @@ export default class Element extends Node implements IElement { * @returns Attribute names. */ public getAttributeNames(): string[] { - return Object.keys(this._attributes); + const attributeNames = []; + for (let i = 0, max = this.attributes.length; i < max; i++) { + attributeNames.push(this.attributes[i].name); + } + return attributeNames; } /** @@ -635,13 +627,7 @@ export default class Element extends Node implements IElement { * @returns True if attribute exists, false otherwise. */ public hasAttributeNS(namespace: string | null, localName: string): boolean { - for (const name of Object.keys(this._attributes)) { - const attribute = this._attributes[name]; - if (attribute.namespaceURI === namespace && attribute.localName === localName) { - return true; - } - } - return false; + return this.attributes.getNamedItemNS(namespace, localName) !== null; } /** @@ -650,7 +636,7 @@ export default class Element extends Node implements IElement { * @returns "true" if the element has attributes. */ public hasAttributes(): boolean { - return Object.keys(this._attributes).length > 0; + return this.attributes.length > 0; } /** @@ -659,10 +645,7 @@ export default class Element extends Node implements IElement { * @param name Name. */ public removeAttribute(name: string): void { - const attribute = this._attributes[this._getAttributeName(name)]; - if (attribute) { - this.removeAttributeNode(attribute); - } + this.attributes.removeNamedItem(name); } /** @@ -672,12 +655,7 @@ export default class Element extends Node implements IElement { * @param localName Local name. */ public removeAttributeNS(namespace: string | null, localName: string): void { - for (const name of Object.keys(this._attributes)) { - const attribute = this._attributes[name]; - if (attribute.namespaceURI === namespace && attribute.localName === localName) { - this.removeAttribute(attribute.name); - } - } + this.attributes.removeNamedItemNS(namespace, localName); } /** @@ -824,67 +802,7 @@ export default class Element extends Node implements IElement { * @returns Replaced attribute. */ public setAttributeNode(attribute: IAttr): IAttr | null { - const name = this._getAttributeName(attribute.name); - const replacedAttribute = this._attributes[name]; - const oldValue = replacedAttribute ? replacedAttribute.value : null; - - attribute.name = name; - (attribute.ownerElement) = this; - (attribute.ownerDocument) = this.ownerDocument; - - if (this.isConnected) { - this.ownerDocument['_cacheID']++; - } - - this._attributes[name] = attribute; - - if (attribute.name === 'class' && this._classList) { - this._classList._updateIndices(); - } - - if (attribute.name === 'id' || attribute.name === 'name') { - if (this.parentNode && (this.parentNode).children && attribute.value !== oldValue) { - if (oldValue) { - (>(this.parentNode).children)._removeNamedItem( - this, - oldValue - ); - } - if (attribute.value) { - (>(this.parentNode).children)._appendNamedItem( - this, - attribute.value - ); - } - } - } - - if ( - this.attributeChangedCallback && - (this.constructor)._observedAttributes && - (this.constructor)._observedAttributes.includes(name) - ) { - this.attributeChangedCallback(name, oldValue, attribute.value); - } - - // MutationObserver - if (this._observers.length > 0) { - for (const observer of this._observers) { - if ( - observer.options.attributes && - (!observer.options.attributeFilter || observer.options.attributeFilter.includes(name)) - ) { - const record = new MutationRecord(); - record.target = this; - record.type = MutationTypeEnum.attributes; - record.attributeName = name; - record.oldValue = observer.options.attributeOldValue ? oldValue : null; - observer.callback([record]); - } - } - } - - return replacedAttribute || null; + return this.attributes.setNamedItem(attribute); } /** @@ -893,8 +811,8 @@ export default class Element extends Node implements IElement { * @param attribute Attribute. * @returns Replaced attribute. */ - public setAttributeNodeNS(attribute: IAttr): IAttr { - return this.setAttributeNode(attribute); + public setAttributeNodeNS(attribute: IAttr): IAttr | null { + return this.attributes.setNamedItemNS(attribute); } /** @@ -903,33 +821,19 @@ export default class Element extends Node implements IElement { * @param name Name. * @returns Replaced attribute. */ - public getAttributeNode(name: string): IAttr { - return this._attributes[this._getAttributeName(name)] || null; + public getAttributeNode(name: string): IAttr | null { + return this.attributes.getNamedItem(name); } /** * Returns a namespaced Attr node. * * @param namespace Namespace. - * @param name Name. + * @param localName Name. * @returns Replaced attribute. */ - public getAttributeNodeNS(namespace: string | null, name: string): IAttr { - const attributeName = this._getAttributeName(name); - if ( - this._attributes[attributeName] && - this._attributes[attributeName].namespaceURI === namespace && - this._attributes[attributeName].localName === attributeName - ) { - return this._attributes[attributeName]; - } - for (const name of Object.keys(this._attributes)) { - const attribute = this._attributes[name]; - if (attribute.namespaceURI === namespace && attribute.localName === attributeName) { - return attribute; - } - } - return null; + public getAttributeNodeNS(namespace: string | null, localName: string): IAttr | null { + return this.attributes.getNamedItemNS(namespace, localName); } /** @@ -938,61 +842,8 @@ export default class Element extends Node implements IElement { * @param attribute Attribute. * @returns Removed attribute. */ - public removeAttributeNode(attribute: IAttr): IAttr { - const removedAttribute = this._attributes[attribute.name]; - - if (removedAttribute !== attribute) { - throw new DOMException( - `Failed to execute 'removeAttributeNode' on 'Element': The node provided is owned by another element.` - ); - } - - delete this._attributes[attribute.name]; - - if (this.isConnected) { - this.ownerDocument['_cacheID']++; - } - - if (attribute.name === 'class' && this._classList) { - this._classList._updateIndices(); - } - - if (attribute.name === 'id' || attribute.name === 'name') { - if (this.parentNode && (this.parentNode).children && attribute.value) { - (>(this.parentNode).children)._removeNamedItem( - this, - attribute.value - ); - } - } - - if ( - this.attributeChangedCallback && - (this.constructor)._observedAttributes && - (this.constructor)._observedAttributes.includes(attribute.name) - ) { - this.attributeChangedCallback(attribute.name, attribute.value, null); - } - - // MutationObserver - if (this._observers.length > 0) { - for (const observer of this._observers) { - if ( - observer.options.attributes && - (!observer.options.attributeFilter || - observer.options.attributeFilter.includes(attribute.name)) - ) { - const record = new MutationRecord(); - record.target = this; - record.type = MutationTypeEnum.attributes; - record.attributeName = attribute.name; - record.oldValue = observer.options.attributeOldValue ? attribute.value : null; - observer.callback([record]); - } - } - } - - return attribute; + public removeAttributeNode(attribute: IAttr): IAttr | null { + return this.attributes.removeNamedItem(attribute.name); } /** @@ -1001,8 +852,8 @@ export default class Element extends Node implements IElement { * @param attribute Attribute. * @returns Removed attribute. */ - public removeAttributeNodeNS(attribute: IAttr): IAttr { - return this.removeAttributeNode(attribute); + public removeAttributeNodeNS(attribute: IAttr): IAttr | null { + return this.attributes.removeNamedItemNS(attribute.namespaceURI, attribute.localName); } /** diff --git a/packages/happy-dom/src/nodes/element/ElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/element/ElementNamedNodeMap.ts new file mode 100644 index 000000000..5e871073f --- /dev/null +++ b/packages/happy-dom/src/nodes/element/ElementNamedNodeMap.ts @@ -0,0 +1,194 @@ +import NamespaceURI from '../../config/NamespaceURI.js'; +import MutationRecord from '../../mutation-observer/MutationRecord.js'; +import MutationTypeEnum from '../../mutation-observer/MutationTypeEnum.js'; +import NamedNodeMap from '../../named-node-map/NamedNodeMap.js'; +import IAttr from '../attr/IAttr.js'; +import IDocument from '../document/IDocument.js'; +import Element from './Element.js'; +import HTMLCollection from './HTMLCollection.js'; +import IElement from './IElement.js'; + +/** + * Named Node Map. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap + */ +export default class ElementNamedNodeMap extends NamedNodeMap { + protected _ownerElement: Element; + + /** + * Constructor. + * + * @param ownerElement Owner element. + */ + constructor(ownerElement: Element) { + super(); + this._ownerElement = ownerElement; + } + + /** + * @override + */ + public override getNamedItem(name: string): IAttr | null { + return this._namedItems[this._getAttributeName(name)] || null; + } + + /** + * @override + */ + public override getNamedItemNS(namespace: string, localName: string): IAttr | null { + return super.getNamedItemNS(namespace, this._getAttributeName(localName)); + } + + /** + * @override + */ + public override setNamedItem(item: IAttr): IAttr | null { + if (!item.name) { + return null; + } + + item.name = this._getAttributeName(item.name); + (item.ownerElement) = this._ownerElement; + (item.ownerDocument) = this._ownerElement.ownerDocument; + + const replacedItem = super.setNamedItem(item); + const oldValue = replacedItem ? replacedItem.value : null; + + if (this._ownerElement.isConnected) { + this._ownerElement.ownerDocument['_cacheID']++; + } + + if (item.name === 'class' && this._ownerElement._classList) { + this._ownerElement._classList._updateIndices(); + } + + if (item.name === 'id' || item.name === 'name') { + if ( + this._ownerElement.parentNode && + (this._ownerElement.parentNode).children && + item.value !== oldValue + ) { + if (oldValue) { + (>( + (this._ownerElement.parentNode).children + ))._removeNamedItem(this._ownerElement, oldValue); + } + if (item.value) { + (>( + (this._ownerElement.parentNode).children + ))._appendNamedItem(this._ownerElement, item.value); + } + } + } + + if ( + this._ownerElement.attributeChangedCallback && + (this._ownerElement.constructor)._observedAttributes && + (this._ownerElement.constructor)._observedAttributes.includes(item.name) + ) { + this._ownerElement.attributeChangedCallback(item.name, oldValue, item.value); + } + + // MutationObserver + if (this._ownerElement._observers.length > 0) { + for (const observer of this._ownerElement._observers) { + if ( + observer.options.attributes && + (!observer.options.attributeFilter || + observer.options.attributeFilter.includes(item.name)) + ) { + const record = new MutationRecord(); + record.target = this._ownerElement; + record.type = MutationTypeEnum.attributes; + record.attributeName = item.name; + record.oldValue = observer.options.attributeOldValue ? oldValue : null; + observer.callback([record]); + } + } + } + + return replacedItem || null; + } + + /** + * @override + */ + public override removeNamedItem(name: string): IAttr | null { + const removedItem = super.removeNamedItem(this._getAttributeName(name)); + + if (!removedItem) { + return null; + } + + if (this._ownerElement.isConnected) { + this._ownerElement.ownerDocument['_cacheID']++; + } + + if (removedItem.name === 'class' && this._ownerElement._classList) { + this._ownerElement._classList._updateIndices(); + } + + if (removedItem.name === 'id' || removedItem.name === 'name') { + if ( + this._ownerElement.parentNode && + (this._ownerElement.parentNode).children && + removedItem.value + ) { + (>( + (this._ownerElement.parentNode).children + ))._removeNamedItem(this._ownerElement, removedItem.value); + } + } + + if ( + this._ownerElement.attributeChangedCallback && + (this._ownerElement.constructor)._observedAttributes && + (this._ownerElement.constructor)._observedAttributes.includes( + removedItem.name + ) + ) { + this._ownerElement.attributeChangedCallback(removedItem.name, removedItem.value, null); + } + + // MutationObserver + if (this._ownerElement._observers.length > 0) { + for (const observer of this._ownerElement._observers) { + if ( + observer.options.attributes && + (!observer.options.attributeFilter || + observer.options.attributeFilter.includes(removedItem.name)) + ) { + const record = new MutationRecord(); + record.target = this._ownerElement; + record.type = MutationTypeEnum.attributes; + record.attributeName = removedItem.name; + record.oldValue = observer.options.attributeOldValue ? removedItem.value : null; + observer.callback([record]); + } + } + } + + return removedItem; + } + + /** + * @override + */ + public override removeNamedItemNS(namespace: string, localName: string): IAttr | null { + return super.removeNamedItemNS(namespace, this._getAttributeName(localName)); + } + + /** + * Returns attribute name. + * + * @param name Name. + * @returns Attribute name based on namespace. + */ + protected _getAttributeName(name): string { + if (this._ownerElement.namespaceURI === NamespaceURI.svg) { + return name; + } + return name.toLowerCase(); + } +} diff --git a/packages/happy-dom/src/nodes/element/ElementUtility.ts b/packages/happy-dom/src/nodes/element/ElementUtility.ts index d53a291f6..75497bf05 100644 --- a/packages/happy-dom/src/nodes/element/ElementUtility.ts +++ b/packages/happy-dom/src/nodes/element/ElementUtility.ts @@ -44,11 +44,12 @@ export default class ElementUtility { if (node.parentNode && (node.parentNode).children) { const index = (node.parentNode).children.indexOf(node); if (index !== -1) { - for (const attribute of NAMED_ITEM_ATTRIBUTES) { - if ((node)._attributes[attribute]) { + for (const attributeName of NAMED_ITEM_ATTRIBUTES) { + const attribute = (node).attributes.getNamedItem(attributeName); + if (attribute) { (>(node.parentNode).children)._removeNamedItem( node, - (node)._attributes[attribute].value + attribute.value ); } } @@ -56,11 +57,12 @@ export default class ElementUtility { } } - for (const attribute of NAMED_ITEM_ATTRIBUTES) { - if ((node)._attributes[attribute]) { + for (const attributeName of NAMED_ITEM_ATTRIBUTES) { + const attribute = (node).attributes.getNamedItem(attributeName); + if (attribute) { (>ancestorNode.children)._appendNamedItem( node, - (node)._attributes[attribute].value + attribute.value ); } } @@ -89,11 +91,12 @@ export default class ElementUtility { if (node.nodeType === NodeTypeEnum.elementNode) { const index = ancestorNode.children.indexOf(node); if (index !== -1) { - for (const attribute of NAMED_ITEM_ATTRIBUTES) { - if ((node)._attributes[attribute]) { + for (const attributeName of NAMED_ITEM_ATTRIBUTES) { + const attribute = (node).attributes.getNamedItem(attributeName); + if (attribute) { (>ancestorNode.children)._removeNamedItem( node, - (node)._attributes[attribute].value + attribute.value ); } } @@ -138,14 +141,12 @@ export default class ElementUtility { if (newNode.parentNode && (newNode.parentNode).children) { const index = (newNode.parentNode).children.indexOf(newNode); if (index !== -1) { - for (const attribute of NAMED_ITEM_ATTRIBUTES) { - if ((newNode)._attributes[attribute]) { + for (const attributeName of NAMED_ITEM_ATTRIBUTES) { + const attribute = (newNode).attributes.getNamedItem(attributeName); + if (attribute) { (>( (newNode.parentNode).children - ))._removeNamedItem( - newNode, - (newNode)._attributes[attribute].value - ); + ))._removeNamedItem(newNode, attribute.value); } } @@ -171,11 +172,12 @@ export default class ElementUtility { } } - for (const attribute of NAMED_ITEM_ATTRIBUTES) { - if ((newNode)._attributes[attribute]) { + for (const attributeName of NAMED_ITEM_ATTRIBUTES) { + const attribute = (newNode).attributes.getNamedItem(attributeName); + if (attribute) { (>ancestorNode.children)._appendNamedItem( newNode, - (newNode)._attributes[attribute].value + attribute.value ); } } diff --git a/packages/happy-dom/src/nodes/element/IElement.ts b/packages/happy-dom/src/nodes/element/IElement.ts index da7b3c8b6..b0b544893 100644 --- a/packages/happy-dom/src/nodes/element/IElement.ts +++ b/packages/happy-dom/src/nodes/element/IElement.ts @@ -238,7 +238,7 @@ export default interface IElement extends IChildNode, INonDocumentTypeChildNode, * @param attribute Attribute. * @returns Replaced attribute. */ - setAttributeNode(attribute: IAttr): IAttr; + setAttributeNode(attribute: IAttr): IAttr | null; /** * The setAttributeNodeNS() method adds a new Attr node to the specified element. @@ -246,7 +246,7 @@ export default interface IElement extends IChildNode, INonDocumentTypeChildNode, * @param attribute Attribute. * @returns Replaced attribute. */ - setAttributeNodeNS(attribute: IAttr): IAttr; + setAttributeNodeNS(attribute: IAttr): IAttr | null; /** * Returns an Attr node. @@ -254,7 +254,7 @@ export default interface IElement extends IChildNode, INonDocumentTypeChildNode, * @param name Name. * @returns Replaced attribute. */ - getAttributeNode(name: string): IAttr; + getAttributeNode(name: string): IAttr | null; /** * Returns a namespaced Attr node. @@ -263,7 +263,7 @@ export default interface IElement extends IChildNode, INonDocumentTypeChildNode, * @param nodeName Node name. * @returns Replaced attribute. */ - getAttributeNodeNS(namespace: string | null, nodeName: string): IAttr; + getAttributeNodeNS(namespace: string | null, nodeName: string): IAttr | null; /** * Removes an Attr node. @@ -279,7 +279,7 @@ export default interface IElement extends IChildNode, INonDocumentTypeChildNode, * @param attribute Attribute. * @returns Removed attribute. */ - removeAttributeNodeNS(attribute: IAttr): IAttr; + removeAttributeNodeNS(attribute: IAttr): IAttr | null; /** * Clones a node. diff --git a/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts b/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts index 65e1750fd..d4c42968a 100644 --- a/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts +++ b/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts @@ -3,8 +3,9 @@ import DOMTokenList from '../../dom-token-list/DOMTokenList.js'; import IDOMTokenList from '../../dom-token-list/IDOMTokenList.js'; import IHTMLAnchorElement from './IHTMLAnchorElement.js'; import { URL } from 'url'; -import IAttr from '../attr/IAttr.js'; import HTMLAnchorElementUtility from './HTMLAnchorElementUtility.js'; +import INamedNodeMap from '../../named-node-map/INamedNodeMap.js'; +import HTMLAnchorElementNamedNodeMap from './HTMLAnchorElementNamedNodeMap.js'; /** * HTML Anchor Element. @@ -13,8 +14,9 @@ import HTMLAnchorElementUtility from './HTMLAnchorElementUtility.js'; * https://developer.mozilla.org/en-US/docs/Web/API/HTMLAnchorElement. */ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAnchorElement { - private _relList: DOMTokenList = null; - private _url: URL | null = null; + public override readonly attributes: INamedNodeMap = new HTMLAnchorElementNamedNodeMap(this); + public _relList: DOMTokenList = null; + public _url: URL | null = null; /** * Returns download. @@ -413,34 +415,4 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho public override toString(): string { return this.href; } - - /** - * @override - */ - public override setAttributeNode(attribute: IAttr): IAttr | null { - const replacedAttribute = super.setAttributeNode(attribute); - - if (attribute.name === 'rel' && this._relList) { - this._relList._updateIndices(); - } else if (attribute.name === 'href') { - this._url = HTMLAnchorElementUtility.getUrl(this.ownerDocument, attribute.value); - } - - return replacedAttribute; - } - - /** - * @override - */ - public override removeAttributeNode(attribute: IAttr): IAttr { - super.removeAttributeNode(attribute); - - if (attribute.name === 'rel' && this._relList) { - this._relList._updateIndices(); - } else if (attribute.name === 'href') { - this._url = null; - } - - return attribute; - } } diff --git a/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElementNamedNodeMap.ts new file mode 100644 index 000000000..b2fde4165 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElementNamedNodeMap.ts @@ -0,0 +1,48 @@ +import IAttr from '../attr/IAttr.js'; +import HTMLElementNamedNodeMap from '../html-element/HTMLElementNamedNodeMap.js'; +import HTMLAnchorElement from './HTMLAnchorElement.js'; +import HTMLAnchorElementUtility from './HTMLAnchorElementUtility.js'; + +/** + * Named Node Map. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap + */ +export default class HTMLAnchorElementNamedNodeMap extends HTMLElementNamedNodeMap { + protected _ownerElement: HTMLAnchorElement; + + /** + * @override + */ + public override setNamedItem(item: IAttr): IAttr | null { + const replacedItem = super.setNamedItem(item); + + if (item.name === 'rel' && this._ownerElement._relList) { + this._ownerElement._relList._updateIndices(); + } else if (item.name === 'href') { + this._ownerElement._url = HTMLAnchorElementUtility.getUrl( + this._ownerElement.ownerDocument, + item.value + ); + } + + return replacedItem || null; + } + + /** + * @override + */ + public override removeNamedItem(name: string): IAttr | null { + const removedItem = super.removeNamedItem(name); + + if (removedItem) { + if (removedItem.name === 'rel' && this._ownerElement._relList) { + this._ownerElement._relList._updateIndices(); + } else if (removedItem.name === 'href') { + this._ownerElement._url = null; + } + } + + return removedItem; + } +} diff --git a/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElement.ts b/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElement.ts index fb56906a2..436b75e14 100644 --- a/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElement.ts +++ b/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElement.ts @@ -1,7 +1,7 @@ import Event from '../../event/Event.js'; import EventPhaseEnum from '../../event/EventPhaseEnum.js'; +import INamedNodeMap from '../../named-node-map/INamedNodeMap.js'; import ValidityState from '../../validity-state/ValidityState.js'; -import IAttr from '../attr/IAttr.js'; import IDocument from '../document/IDocument.js'; import HTMLElement from '../html-element/HTMLElement.js'; import HTMLFormElement from '../html-form-element/HTMLFormElement.js'; @@ -11,6 +11,7 @@ import INode from '../node/INode.js'; import INodeList from '../node/INodeList.js'; import NodeList from '../node/NodeList.js'; import IShadowRoot from '../shadow-root/IShadowRoot.js'; +import HTMLButtonElementNamedNodeMap from './HTMLButtonElementNamedNodeMap.js'; import IHTMLButtonElement from './IHTMLButtonElement.js'; const BUTTON_TYPES = ['submit', 'reset', 'button', 'menu']; @@ -22,6 +23,7 @@ const BUTTON_TYPES = ['submit', 'reset', 'button', 'menu']; * https://developer.mozilla.org/en-US/docs/Web/API/HTMLButtonElement. */ export default class HTMLButtonElement extends HTMLElement implements IHTMLButtonElement { + public override readonly attributes: INamedNodeMap = new HTMLButtonElementNamedNodeMap(this); public readonly validationMessage = ''; public readonly validity = new ValidityState(this); @@ -238,38 +240,6 @@ export default class HTMLButtonElement extends HTMLElement implements IHTMLButto return returnValue; } - /** - * @override - */ - public override setAttributeNode(attribute: IAttr): IAttr | null { - const replacedAttribute = super.setAttributeNode(attribute); - const oldValue = replacedAttribute ? replacedAttribute.value : null; - - if ((attribute.name === 'id' || attribute.name === 'name') && this._formNode) { - if (oldValue) { - (this._formNode)._removeFormControlItem(this, oldValue); - } - if (attribute.value) { - (this._formNode)._appendFormControlItem(this, attribute.value); - } - } - - return replacedAttribute; - } - - /** - * @override - */ - public override removeAttributeNode(attribute: IAttr): IAttr { - super.removeAttributeNode(attribute); - - if ((attribute.name === 'id' || attribute.name === 'name') && this._formNode) { - (this._formNode)._removeFormControlItem(this, attribute.value); - } - - return attribute; - } - /** * @override */ diff --git a/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElementNamedNodeMap.ts new file mode 100644 index 000000000..5c5a6e74f --- /dev/null +++ b/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElementNamedNodeMap.ts @@ -0,0 +1,56 @@ +import IAttr from '../attr/IAttr.js'; +import HTMLElementNamedNodeMap from '../html-element/HTMLElementNamedNodeMap.js'; +import HTMLFormElement from '../html-form-element/HTMLFormElement.js'; +import HTMLButtonElement from './HTMLButtonElement.js'; + +/** + * Named Node Map. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap + */ +export default class HTMLButtonElementNamedNodeMap extends HTMLElementNamedNodeMap { + protected _ownerElement: HTMLButtonElement; + + /** + * @override + */ + public override setNamedItem(item: IAttr): IAttr | null { + const replacedItem = super.setNamedItem(item); + + if ((item.name === 'id' || item.name === 'name') && this._ownerElement._formNode) { + if (replacedItem?.value) { + (this._ownerElement._formNode)._removeFormControlItem( + this._ownerElement, + replacedItem.value + ); + } + if (item.value) { + (this._ownerElement._formNode)._appendFormControlItem( + this._ownerElement, + item.value + ); + } + } + + return replacedItem || null; + } + + /** + * @override + */ + public override removeNamedItem(name: string): IAttr | null { + const removedItem = super.removeNamedItem(name); + + if ( + (removedItem.name === 'id' || removedItem.name === 'name') && + this._ownerElement._formNode + ) { + (this._ownerElement._formNode)._removeFormControlItem( + this._ownerElement, + removedItem.value + ); + } + + return removedItem; + } +} diff --git a/packages/happy-dom/src/nodes/html-element/HTMLElement.ts b/packages/happy-dom/src/nodes/html-element/HTMLElement.ts index add408753..03257850b 100644 --- a/packages/happy-dom/src/nodes/html-element/HTMLElement.ts +++ b/packages/happy-dom/src/nodes/html-element/HTMLElement.ts @@ -1,13 +1,14 @@ import Element from '../element/Element.js'; import IHTMLElement from './IHTMLElement.js'; import CSSStyleDeclaration from '../../css/declaration/CSSStyleDeclaration.js'; -import IAttr from '../attr/IAttr.js'; import PointerEvent from '../../event/events/PointerEvent.js'; import Dataset from '../element/Dataset.js'; import NodeTypeEnum from '../node/NodeTypeEnum.js'; import DOMException from '../../exception/DOMException.js'; import Event from '../../event/Event.js'; import HTMLElementUtility from './HTMLElementUtility.js'; +import INamedNodeMap from '../../named-node-map/INamedNodeMap.js'; +import HTMLElementNamedNodeMap from './HTMLElementNamedNodeMap.js'; /** * HTML Element. @@ -16,6 +17,7 @@ import HTMLElementUtility from './HTMLElementUtility.js'; * https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement. */ export default class HTMLElement extends Element implements IHTMLElement { + public override readonly attributes: INamedNodeMap = new HTMLElementNamedNodeMap(this); public readonly accessKey = ''; public readonly accessKeyLabel = ''; public readonly contentEditable = 'inherit'; @@ -27,7 +29,7 @@ export default class HTMLElement extends Element implements IHTMLElement { public readonly clientHeight = 0; public readonly clientWidth = 0; - private _style: CSSStyleDeclaration = null; + public _style: CSSStyleDeclaration = null; private _dataset: Dataset = null; // Events @@ -322,32 +324,6 @@ export default class HTMLElement extends Element implements IHTMLElement { HTMLElementUtility.focus(this); } - /** - * @override - */ - public setAttributeNode(attribute: IAttr): IAttr | null { - const replacedAttribute = super.setAttributeNode(attribute); - - if (attribute.name === 'style' && this._style) { - this._style.cssText = attribute.value; - } - - return replacedAttribute; - } - - /** - * @override - */ - public removeAttributeNode(attribute: IAttr): IAttr { - super.removeAttributeNode(attribute); - - if (attribute.name === 'style' && this._style) { - this._style.cssText = ''; - } - - return attribute; - } - /** * @override */ diff --git a/packages/happy-dom/src/nodes/html-element/HTMLElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-element/HTMLElementNamedNodeMap.ts new file mode 100644 index 000000000..197b93c0c --- /dev/null +++ b/packages/happy-dom/src/nodes/html-element/HTMLElementNamedNodeMap.ts @@ -0,0 +1,38 @@ +import IAttr from '../attr/IAttr.js'; +import ElementNamedNodeMap from '../element/ElementNamedNodeMap.js'; +import HTMLElement from './HTMLElement.js'; + +/** + * Named Node Map. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap + */ +export default class HTMLElementNamedNodeMap extends ElementNamedNodeMap { + protected _ownerElement: HTMLElement; + + /** + * @override + */ + public override setNamedItem(item: IAttr): IAttr | null { + const replacedItem = super.setNamedItem(item); + + if (item.name === 'style' && this._ownerElement._style) { + this._ownerElement._style.cssText = item.value; + } + + return replacedItem || null; + } + + /** + * @override + */ + public override removeNamedItem(name: string): IAttr | null { + const removedItem = super.removeNamedItem(name); + + if (removedItem && removedItem.name === 'style' && this._ownerElement._style) { + this._ownerElement._style.cssText = ''; + } + + return removedItem; + } +} diff --git a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts index 8efa1ff71..93f4dbba0 100644 --- a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts +++ b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts @@ -6,7 +6,8 @@ import INode from '../node/INode.js'; import IFrameCrossOriginWindow from './IFrameCrossOriginWindow.js'; import IHTMLIFrameElement from './IHTMLIFrameElement.js'; import HTMLIFrameUtility from './HTMLIFrameUtility.js'; -import IAttr from '../attr/IAttr.js'; +import INamedNodeMap from '../../named-node-map/INamedNodeMap.js'; +import HTMLIFrameElementNamedNodeMap from './HTMLIFrameElementNamedNodeMap.js'; /** * HTML Iframe Element. @@ -15,6 +16,8 @@ import IAttr from '../attr/IAttr.js'; * https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement. */ export default class HTMLIFrameElement extends HTMLElement implements IHTMLIFrameElement { + public override readonly attributes: INamedNodeMap = new HTMLIFrameElementNamedNodeMap(this); + // Events public onload: (event: Event) => void | null = null; public onerror: (event: Event) => void | null = null; @@ -180,23 +183,6 @@ export default class HTMLIFrameElement extends HTMLElement implements IHTMLIFram } } - /** - * @override - */ - public override setAttributeNode(attribute: IAttr): IAttr | null { - const replacedAttribute = super.setAttributeNode(attribute); - - if ( - attribute.name === 'src' && - attribute.value && - attribute.value !== replacedAttribute?.value - ) { - HTMLIFrameUtility.loadPage(this); - } - - return replacedAttribute; - } - /** * Clones a node. * diff --git a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementNamedNodeMap.ts new file mode 100644 index 000000000..5dff9daf5 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementNamedNodeMap.ts @@ -0,0 +1,26 @@ +import IAttr from '../attr/IAttr.js'; +import HTMLElementNamedNodeMap from '../html-element/HTMLElementNamedNodeMap.js'; +import HTMLIFrameElement from './HTMLIFrameElement.js'; +import HTMLIFrameUtility from './HTMLIFrameUtility.js'; + +/** + * Named Node Map. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap + */ +export default class HTMLIFrameElementNamedNodeMap extends HTMLElementNamedNodeMap { + protected _ownerElement: HTMLIFrameElement; + + /** + * @override + */ + public override setNamedItem(item: IAttr): IAttr | null { + const replacedAttribute = super.setNamedItem(item); + + if (item.name === 'src' && item.value && item.value !== replacedAttribute?.value) { + HTMLIFrameUtility.loadPage(this._ownerElement); + } + + return replacedAttribute || null; + } +} diff --git a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts index 27f760b08..1f4a6bc6f 100644 --- a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts +++ b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts @@ -13,7 +13,6 @@ import HTMLInputElementValueStepping from './HTMLInputElementValueStepping.js'; import FileList from './FileList.js'; import File from '../../file/File.js'; import IFileList from './IFileList.js'; -import IAttr from '../attr/IAttr.js'; import INode from '../node/INode.js'; import HTMLFormElement from '../html-form-element/HTMLFormElement.js'; import INodeList from '../node/INodeList.js'; @@ -23,6 +22,8 @@ import IShadowRoot from '../shadow-root/IShadowRoot.js'; import NodeList from '../node/NodeList.js'; import EventPhaseEnum from '../../event/EventPhaseEnum.js'; import { dateIsoWeek, isoWeekDate } from './HTMLInputDateUtility.js'; +import INamedNodeMap from '../../named-node-map/INamedNodeMap.js'; +import HTMLInputElementNamedNodeMap from './HTMLInputElementNamedNodeMap.js'; /** * HTML Input Element. @@ -34,6 +35,8 @@ import { dateIsoWeek, isoWeekDate } from './HTMLInputDateUtility.js'; * https://github.com/jsdom/jsdom/blob/master/lib/jsdom/living/nodes/nodes/HTMLInputElement-impl.js (MIT licensed). */ export default class HTMLInputElement extends HTMLElement implements IHTMLInputElement { + public override readonly attributes: INamedNodeMap = new HTMLInputElementNamedNodeMap(this); + // Related to parent form. public formAction = ''; public formMethod = ''; @@ -1224,38 +1227,6 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE return returnValue; } - /** - * @override - */ - public override setAttributeNode(attribute: IAttr): IAttr | null { - const replacedAttribute = super.setAttributeNode(attribute); - const oldValue = replacedAttribute ? replacedAttribute.value : null; - - if ((attribute.name === 'id' || attribute.name === 'name') && this._formNode) { - if (oldValue) { - (this._formNode)._removeFormControlItem(this, oldValue); - } - if (attribute.value) { - (this._formNode)._appendFormControlItem(this, attribute.value); - } - } - - return replacedAttribute; - } - - /** - * @override - */ - public override removeAttributeNode(attribute: IAttr): IAttr { - super.removeAttributeNode(attribute); - - if ((attribute.name === 'id' || attribute.name === 'name') && this._formNode) { - (this._formNode)._removeFormControlItem(this, attribute.value); - } - - return attribute; - } - /** * @override */ diff --git a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElementNamedNodeMap.ts new file mode 100644 index 000000000..25d1b11f8 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElementNamedNodeMap.ts @@ -0,0 +1,57 @@ +import IAttr from '../attr/IAttr.js'; +import HTMLElementNamedNodeMap from '../html-element/HTMLElementNamedNodeMap.js'; +import HTMLFormElement from '../html-form-element/HTMLFormElement.js'; +import HTMLInputElement from './HTMLInputElement.js'; + +/** + * Named Node Map. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap + */ +export default class HTMLInputElementNamedNodeMap extends HTMLElementNamedNodeMap { + protected _ownerElement: HTMLInputElement; + + /** + * @override + */ + public override setNamedItem(item: IAttr): IAttr | null { + const replacedItem = super.setNamedItem(item); + + if ((item.name === 'id' || item.name === 'name') && this._ownerElement._formNode) { + if (replacedItem && replacedItem.value) { + (this._ownerElement._formNode)._removeFormControlItem( + this._ownerElement, + replacedItem.value + ); + } + if (item.value) { + (this._ownerElement._formNode)._appendFormControlItem( + this._ownerElement, + item.value + ); + } + } + + return replacedItem || null; + } + + /** + * @override + */ + public override removeNamedItem(name: string): IAttr | null { + const removedItem = super.removeNamedItem(name); + + if ( + removedItem && + (removedItem.name === 'id' || removedItem.name === 'name') && + this._ownerElement._formNode + ) { + (this._ownerElement._formNode)._removeFormControlItem( + this._ownerElement, + removedItem.value + ); + } + + return removedItem; + } +} 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 53c04e9bf..04d732761 100644 --- a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts +++ b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts @@ -1,4 +1,3 @@ -import IAttr from '../attr/IAttr.js'; import CSSStyleSheet from '../../css/CSSStyleSheet.js'; import HTMLElement from '../html-element/HTMLElement.js'; import IHTMLLinkElement from './IHTMLLinkElement.js'; @@ -8,6 +7,8 @@ import INode from '../../nodes/node/INode.js'; import DOMTokenList from '../../dom-token-list/DOMTokenList.js'; import IDOMTokenList from '../../dom-token-list/IDOMTokenList.js'; import HTMLLinkElementUtility from './HTMLLinkElementUtility.js'; +import INamedNodeMap from '../../named-node-map/INamedNodeMap.js'; +import HTMLLinkElementNamedNodeMap from './HTMLLinkElementNamedNodeMap.js'; /** * HTML Link Element. @@ -16,11 +17,12 @@ import HTMLLinkElementUtility from './HTMLLinkElementUtility.js'; * https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement. */ export default class HTMLLinkElement extends HTMLElement implements IHTMLLinkElement { + public override readonly attributes: INamedNodeMap = new HTMLLinkElementNamedNodeMap(this); public onerror: (event: ErrorEvent) => void = null; public onload: (event: Event) => void = null; public readonly sheet: CSSStyleSheet = null; public _evaluateCSS = true; - private _relList: DOMTokenList = null; + public _relList: DOMTokenList = null; /** * Returns rel list. @@ -178,36 +180,6 @@ export default class HTMLLinkElement extends HTMLElement implements IHTMLLinkEle this.setAttribute('type', type); } - /** - * @override - */ - public override setAttributeNode(attribute: IAttr): IAttr | null { - const replacedAttribute = super.setAttributeNode(attribute); - - if (attribute.name === 'rel' && this._relList) { - this._relList._updateIndices(); - } - - if (attribute.name === 'rel' || attribute.name === 'href') { - HTMLLinkElementUtility.loadExternalStylesheet(this); - } - - return replacedAttribute; - } - - /** - * @override - */ - public override removeAttributeNode(attribute: IAttr): IAttr { - super.removeAttributeNode(attribute); - - if (attribute.name === 'rel' && this._relList) { - this._relList._updateIndices(); - } - - return attribute; - } - /** * @override */ diff --git a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementNamedNodeMap.ts new file mode 100644 index 000000000..f2add2f92 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementNamedNodeMap.ts @@ -0,0 +1,43 @@ +import IAttr from '../attr/IAttr.js'; +import HTMLElementNamedNodeMap from '../html-element/HTMLElementNamedNodeMap.js'; +import HTMLLinkElement from './HTMLLinkElement.js'; +import HTMLLinkElementUtility from './HTMLLinkElementUtility.js'; + +/** + * Named Node Map. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap + */ +export default class HTMLLinkElementNamedNodeMap extends HTMLElementNamedNodeMap { + protected _ownerElement: HTMLLinkElement; + + /** + * @override + */ + public override setNamedItem(item: IAttr): IAttr | null { + const replacedItem = super.setNamedItem(item); + + if (item.name === 'rel' && this._ownerElement._relList) { + this._ownerElement._relList._updateIndices(); + } + + if (item.name === 'rel' || item.name === 'href') { + HTMLLinkElementUtility.loadExternalStylesheet(this._ownerElement); + } + + return replacedItem || null; + } + + /** + * @override + */ + public override removeNamedItem(name: string): IAttr | null { + const removedItem = super.removeNamedItem(name); + + if (removedItem && removedItem.name === 'rel' && this._ownerElement._relList) { + this._ownerElement._relList._updateIndices(); + } + + return removedItem; + } +} diff --git a/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts b/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts index 2f5208f09..f676de1aa 100644 --- a/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts +++ b/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts @@ -1,8 +1,9 @@ -import IAttr from '../attr/IAttr.js'; +import INamedNodeMap from '../../named-node-map/INamedNodeMap.js'; import HTMLElement from '../html-element/HTMLElement.js'; import IHTMLFormElement from '../html-form-element/IHTMLFormElement.js'; import HTMLSelectElement from '../html-select-element/HTMLSelectElement.js'; import INode from '../node/INode.js'; +import HTMLOptionElementNamedNodeMap from './HTMLOptionElementNamedNodeMap.js'; import IHTMLOptionElement from './IHTMLOptionElement.js'; /** @@ -12,6 +13,7 @@ import IHTMLOptionElement from './IHTMLOptionElement.js'; * https://developer.mozilla.org/en-US/docs/Web/API/HTMLOptionElement. */ export default class HTMLOptionElement extends HTMLElement implements IHTMLOptionElement { + public override readonly attributes: INamedNodeMap = new HTMLOptionElementNamedNodeMap(this); public _index: number; public _selectedness = false; public _dirtyness = false; @@ -117,48 +119,6 @@ export default class HTMLOptionElement extends HTMLElement implements IHTMLOptio this.setAttribute('value', value); } - /** - * @override - */ - public setAttributeNode(attribute: IAttr): IAttr | null { - const replacedAttribute = super.setAttributeNode(attribute); - - if ( - !this._dirtyness && - attribute.name === 'selected' && - replacedAttribute?.value !== attribute.value - ) { - const selectNode = this._selectNode; - - this._selectedness = true; - - if (selectNode) { - selectNode._updateOptionItems(this); - } - } - - return replacedAttribute; - } - - /** - * @override - */ - public removeAttributeNode(attribute: IAttr): IAttr { - super.removeAttributeNode(attribute); - - if (!this._dirtyness && attribute.name === 'selected') { - const selectNode = this._selectNode; - - this._selectedness = false; - - if (selectNode) { - selectNode._updateOptionItems(); - } - } - - return attribute; - } - /** * @override */ diff --git a/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElementNamedNodeMap.ts new file mode 100644 index 000000000..be87c3e2e --- /dev/null +++ b/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElementNamedNodeMap.ts @@ -0,0 +1,55 @@ +import IAttr from '../attr/IAttr.js'; +import HTMLElementNamedNodeMap from '../html-element/HTMLElementNamedNodeMap.js'; +import HTMLSelectElement from '../html-select-element/HTMLSelectElement.js'; +import HTMLOptionElement from './HTMLOptionElement.js'; + +/** + * Named Node Map. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap + */ +export default class HTMLOptionElementNamedNodeMap extends HTMLElementNamedNodeMap { + protected _ownerElement: HTMLOptionElement; + + /** + * @override + */ + public override setNamedItem(item: IAttr): IAttr | null { + const replacedItem = super.setNamedItem(item); + + if ( + !this._ownerElement._dirtyness && + item.name === 'selected' && + replacedItem?.value !== item.value + ) { + const selectNode = this._ownerElement._selectNode; + + this._ownerElement._selectedness = true; + + if (selectNode) { + selectNode._updateOptionItems(this._ownerElement); + } + } + + return replacedItem || null; + } + + /** + * @override + */ + public override removeNamedItem(name: string): IAttr | null { + const removedItem = super.removeNamedItem(name); + + if (removedItem && !this._ownerElement._dirtyness && removedItem.name === 'selected') { + const selectNode = this._ownerElement._selectNode; + + this._ownerElement._selectedness = false; + + if (selectNode) { + selectNode._updateOptionItems(); + } + } + + return removedItem; + } +} diff --git a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts index 4ec9d9eb9..575f0e34f 100644 --- a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts +++ b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts @@ -1,10 +1,11 @@ -import IAttr from '../attr/IAttr.js'; import HTMLElement from '../html-element/HTMLElement.js'; import IHTMLScriptElement from './IHTMLScriptElement.js'; import HTMLScriptElementUtility from './HTMLScriptElementUtility.js'; import Event from '../../event/Event.js'; import ErrorEvent from '../../event/events/ErrorEvent.js'; import INode from '../../nodes/node/INode.js'; +import INamedNodeMap from '../../named-node-map/INamedNodeMap.js'; +import HTMLScriptElementNamedNodeMap from './HTMLScriptElementNamedNodeMap.js'; /** * HTML Script Element. @@ -13,6 +14,7 @@ import INode from '../../nodes/node/INode.js'; * https://developer.mozilla.org/en-US/docs/Web/API/HTMLScriptElement. */ export default class HTMLScriptElement extends HTMLElement implements IHTMLScriptElement { + public override readonly attributes: INamedNodeMap = new HTMLScriptElementNamedNodeMap(this); public onerror: (event: ErrorEvent) => void = null; public onload: (event: Event) => void = null; public _evaluateScript = true; @@ -151,23 +153,6 @@ export default class HTMLScriptElement extends HTMLElement implements IHTMLScrip this.textContent = text; } - /** - * The setAttributeNode() method adds a new Attr node to the specified element. - * - * @override - * @param attribute Attribute. - * @returns Replaced attribute. - */ - public setAttributeNode(attribute: IAttr): IAttr | null { - const replacedAttribute = super.setAttributeNode(attribute); - - if (attribute.name === 'src' && attribute.value !== null && this.isConnected) { - HTMLScriptElementUtility.loadExternalScript(this); - } - - return replacedAttribute; - } - /** * Clones a node. * diff --git a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementNamedNodeMap.ts new file mode 100644 index 000000000..69be7f9f7 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementNamedNodeMap.ts @@ -0,0 +1,26 @@ +import IAttr from '../attr/IAttr.js'; +import HTMLElementNamedNodeMap from '../html-element/HTMLElementNamedNodeMap.js'; +import HTMLScriptElement from './HTMLScriptElement.js'; +import HTMLScriptElementUtility from './HTMLScriptElementUtility.js'; + +/** + * Named Node Map. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap + */ +export default class HTMLScriptElementNamedNodeMap extends HTMLElementNamedNodeMap { + protected _ownerElement: HTMLScriptElement; + + /** + * @override + */ + public override setNamedItem(item: IAttr): IAttr | null { + const replacedItem = super.setNamedItem(item); + + if (item.name === 'src' && item.value !== null && this._ownerElement.isConnected) { + HTMLScriptElementUtility.loadExternalScript(this._ownerElement); + } + + return replacedItem || null; + } +} diff --git a/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts b/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts index 76fe685a2..d32eee327 100644 --- a/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts +++ b/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts @@ -13,11 +13,12 @@ import IHTMLOptionsCollection from './IHTMLOptionsCollection.js'; import INode from '../node/INode.js'; import NodeTypeEnum from '../node/NodeTypeEnum.js'; import HTMLFormElement from '../html-form-element/HTMLFormElement.js'; -import IAttr from '../attr/IAttr.js'; import IHTMLCollection from '../element/IHTMLCollection.js'; import NodeList from '../node/NodeList.js'; import IDocument from '../document/IDocument.js'; import IShadowRoot from '../shadow-root/IShadowRoot.js'; +import INamedNodeMap from '../../named-node-map/INamedNodeMap.js'; +import HTMLSelectElementNamedNodeMap from './HTMLSelectElementNamedNodeMap.js'; /** * HTML Select Element. @@ -26,6 +27,8 @@ import IShadowRoot from '../shadow-root/IShadowRoot.js'; * https://developer.mozilla.org/en-US/docs/Web/API/HTMLSelectElement. */ export default class HTMLSelectElement extends HTMLElement implements IHTMLSelectElement { + public override readonly attributes: INamedNodeMap = new HTMLSelectElementNamedNodeMap(this); + // Public properties. public readonly length = 0; public readonly options: IHTMLOptionsCollection = new HTMLOptionsCollection(this); @@ -402,38 +405,6 @@ export default class HTMLSelectElement extends HTMLElement implements IHTMLSelec } } - /** - * @override - */ - public override setAttributeNode(attribute: IAttr): IAttr | null { - const replacedAttribute = super.setAttributeNode(attribute); - const oldValue = replacedAttribute ? replacedAttribute.value : null; - - if ((attribute.name === 'id' || attribute.name === 'name') && this._formNode) { - if (oldValue) { - (this._formNode)._removeFormControlItem(this, oldValue); - } - if (attribute.value) { - (this._formNode)._appendFormControlItem(this, attribute.value); - } - } - - return replacedAttribute; - } - - /** - * @override - */ - public override removeAttributeNode(attribute: IAttr): IAttr { - super.removeAttributeNode(attribute); - - if ((attribute.name === 'id' || attribute.name === 'name') && this._formNode) { - (this._formNode)._removeFormControlItem(this, attribute.value); - } - - return attribute; - } - /** * @override */ diff --git a/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElementNamedNodeMap.ts new file mode 100644 index 000000000..63e319218 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElementNamedNodeMap.ts @@ -0,0 +1,57 @@ +import IAttr from '../attr/IAttr.js'; +import HTMLElementNamedNodeMap from '../html-element/HTMLElementNamedNodeMap.js'; +import HTMLFormElement from '../html-form-element/HTMLFormElement.js'; +import HTMLSelectElement from './HTMLSelectElement.js'; + +/** + * Named Node Map. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap + */ +export default class HTMLSelectElementNamedNodeMap extends HTMLElementNamedNodeMap { + protected _ownerElement: HTMLSelectElement; + + /** + * @override + */ + public override setNamedItem(item: IAttr): IAttr | null { + const replacedItem = super.setNamedItem(item); + + if ((item.name === 'id' || item.name === 'name') && this._ownerElement._formNode) { + if (replacedItem && replacedItem.value) { + (this._ownerElement._formNode)._removeFormControlItem( + this._ownerElement, + replacedItem.value + ); + } + if (item.value) { + (this._ownerElement._formNode)._appendFormControlItem( + this._ownerElement, + item.value + ); + } + } + + return replacedItem || null; + } + + /** + * @override + */ + public override removeNamedItem(name: string): IAttr | null { + const removedItem = super.removeNamedItem(name); + + if ( + removedItem && + (removedItem.name === 'id' || removedItem.name === 'name') && + this._ownerElement._formNode + ) { + (this._ownerElement._formNode)._removeFormControlItem( + this._ownerElement, + removedItem.value + ); + } + + return removedItem; + } +} diff --git a/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts b/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts index c28251fa8..634d8a1ce 100644 --- a/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts +++ b/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts @@ -1,7 +1,6 @@ import Event from '../../event/Event.js'; import DOMException from '../../exception/DOMException.js'; import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; -import IAttr from '../attr/IAttr.js'; import HTMLElement from '../html-element/HTMLElement.js'; import HTMLFormElement from '../html-form-element/HTMLFormElement.js'; import IHTMLFormElement from '../html-form-element/IHTMLFormElement.js'; @@ -15,6 +14,8 @@ import IHTMLLabelElement from '../html-label-element/IHTMLLabelElement.js'; import IDocument from '../document/IDocument.js'; import IShadowRoot from '../shadow-root/IShadowRoot.js'; import NodeList from '../node/NodeList.js'; +import INamedNodeMap from '../../named-node-map/INamedNodeMap.js'; +import HTMLTextAreaElementNamedNodeMap from './HTMLTextAreaElementNamedNodeMap.js'; /** * HTML Text Area Element. @@ -23,6 +24,7 @@ import NodeList from '../node/NodeList.js'; * https://developer.mozilla.org/en-US/docs/Web/API/HTMLTextAreaElement. */ export default class HTMLTextAreaElement extends HTMLElement implements IHTMLTextAreaElement { + public override readonly attributes: INamedNodeMap = new HTMLTextAreaElementNamedNodeMap(this); public readonly type = 'textarea'; public readonly validationMessage = ''; public readonly validity = new ValidityState(this); @@ -588,38 +590,6 @@ export default class HTMLTextAreaElement extends HTMLElement implements IHTMLTex } } - /** - * @override - */ - public override setAttributeNode(attribute: IAttr): IAttr | null { - const replacedAttribute = super.setAttributeNode(attribute); - const oldValue = replacedAttribute ? replacedAttribute.value : null; - - if ((attribute.name === 'id' || attribute.name === 'name') && this._formNode) { - if (oldValue) { - (this._formNode)._removeFormControlItem(this, oldValue); - } - if (attribute.value) { - (this._formNode)._appendFormControlItem(this, attribute.value); - } - } - - return replacedAttribute; - } - - /** - * @override - */ - public override removeAttributeNode(attribute: IAttr): IAttr { - super.removeAttributeNode(attribute); - - if ((attribute.name === 'id' || attribute.name === 'name') && this._formNode) { - (this._formNode)._removeFormControlItem(this, attribute.value); - } - - return attribute; - } - /** * @override */ diff --git a/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElementNamedNodeMap.ts new file mode 100644 index 000000000..4bc3ffb79 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElementNamedNodeMap.ts @@ -0,0 +1,57 @@ +import IAttr from '../attr/IAttr.js'; +import HTMLElementNamedNodeMap from '../html-element/HTMLElementNamedNodeMap.js'; +import HTMLFormElement from '../html-form-element/HTMLFormElement.js'; +import HTMLTextAreaElement from './HTMLTextAreaElement.js'; + +/** + * Named Node Map. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap + */ +export default class HTMLTextAreaElementNamedNodeMap extends HTMLElementNamedNodeMap { + protected _ownerElement: HTMLTextAreaElement; + + /** + * @override + */ + public override setNamedItem(item: IAttr): IAttr | null { + const replacedItem = super.setNamedItem(item); + + if ((item.name === 'id' || item.name === 'name') && this._ownerElement._formNode) { + if (replacedItem && replacedItem.value) { + (this._ownerElement._formNode)._removeFormControlItem( + this._ownerElement, + replacedItem.value + ); + } + if (item.value) { + (this._ownerElement._formNode)._appendFormControlItem( + this._ownerElement, + item.value + ); + } + } + + return replacedItem || null; + } + + /** + * @override + */ + public override removeNamedItem(name: string): IAttr | null { + const removedItem = super.removeNamedItem(name); + + if ( + removedItem && + (removedItem.name === 'id' || removedItem.name === 'name') && + this._ownerElement._formNode + ) { + (this._ownerElement._formNode)._removeFormControlItem( + this._ownerElement, + removedItem.value + ); + } + + return removedItem; + } +} diff --git a/packages/happy-dom/src/nodes/html-unknown-element/HTMLUnknownElement.ts b/packages/happy-dom/src/nodes/html-unknown-element/HTMLUnknownElement.ts index 59d9c8728..ab292bad2 100644 --- a/packages/happy-dom/src/nodes/html-unknown-element/HTMLUnknownElement.ts +++ b/packages/happy-dom/src/nodes/html-unknown-element/HTMLUnknownElement.ts @@ -6,6 +6,7 @@ import IHTMLCollection from '../element/IHTMLCollection.js'; import IElement from '../element/IElement.js'; import NodeList from '../node/NodeList.js'; import HTMLCollection from '../element/HTMLCollection.js'; +import HTMLElementNamedNodeMap from '../html-element/HTMLElementNamedNodeMap.js'; /** * HTML Unknown Element. @@ -43,7 +44,10 @@ export default class HTMLUnknownElement extends HTMLElement implements IHTMLElem newElement._textAreaNode = this._textAreaNode; newElement._observers = this._observers; newElement._isValue = this._isValue; - newElement._attributes = this._attributes; + + for (let i = 0, max = this.attributes.length; i < max; i++) { + newElement.attributes.setNamedItem(this.attributes[i]); + } (>this.childNodes) = new NodeList(); (>this.children) = new HTMLCollection(); @@ -53,7 +57,7 @@ export default class HTMLUnknownElement extends HTMLElement implements IHTMLElem this._textAreaNode = null; this._observers = []; this._isValue = null; - this._attributes = {}; + (this.attributes) = new HTMLElementNamedNodeMap(this); for (let i = 0, max = this.parentNode.childNodes.length; i < max; i++) { if (this.parentNode.childNodes[i] === this) { diff --git a/packages/happy-dom/src/nodes/node/NodeUtility.ts b/packages/happy-dom/src/nodes/node/NodeUtility.ts index b657ac5ea..2120c4169 100644 --- a/packages/happy-dom/src/nodes/node/NodeUtility.ts +++ b/packages/happy-dom/src/nodes/node/NodeUtility.ts @@ -395,33 +395,16 @@ 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 lengthA = listA.length; - const lengthB = listB.length; - - if (lengthA !== lengthB) { - return false; - } - - for (let i = 0; i < lengthA; ++i) { - const attrA = listA[i]; - - if ( - !listB.some((attrB) => { - return ( - (typeof attrA === 'number' && typeof attrB === 'number' && attrA === attrB) || - (typeof attrA === 'object' && - typeof attrB === 'object' && - NodeUtility.isEqualNode(attrA, attrB)) - ); - }) - ) { + for (let i = 0, max = elementA.attributes.length; i < max; i++) { + const attributeA = elementA.attributes[i]; + const attributeB = elementB.attributes.getNamedItemNS( + attributeA.namespaceURI, + attributeA.localName + ); + if (!attributeB || attributeB.value !== attributeA.value) { return false; } } - return true; } diff --git a/packages/happy-dom/src/nodes/svg-element/SVGElement.ts b/packages/happy-dom/src/nodes/svg-element/SVGElement.ts index d311a4f30..61fa568e3 100644 --- a/packages/happy-dom/src/nodes/svg-element/SVGElement.ts +++ b/packages/happy-dom/src/nodes/svg-element/SVGElement.ts @@ -2,10 +2,11 @@ import CSSStyleDeclaration from '../../css/declaration/CSSStyleDeclaration.js'; import Element from '../element/Element.js'; import ISVGElement from './ISVGElement.js'; import ISVGSVGElement from './ISVGSVGElement.js'; -import IAttr from '../attr/IAttr.js'; import Event from '../../event/Event.js'; import Dataset from '../element/Dataset.js'; import HTMLElementUtility from '../html-element/HTMLElementUtility.js'; +import INamedNodeMap from '../../named-node-map/INamedNodeMap.js'; +import SVGElementNamedNodeMap from './SVGElementNamedNodeMap.js'; /** * SVG Element. @@ -14,6 +15,8 @@ import HTMLElementUtility from '../html-element/HTMLElementUtility.js'; * https://developer.mozilla.org/en-US/docs/Web/API/SVGElement. */ export default class SVGElement extends Element implements ISVGElement { + public override readonly attributes: INamedNodeMap = new SVGElementNamedNodeMap(this); + // Events public onabort: (event: Event) => void | null = null; public onerror: (event: Event) => void | null = null; @@ -23,7 +26,7 @@ export default class SVGElement extends Element implements ISVGElement { public onunload: (event: Event) => void | null = null; // Private properties - private _style: CSSStyleDeclaration = null; + public _style: CSSStyleDeclaration = null; private _dataset: Dataset = null; /** @@ -109,30 +112,4 @@ export default class SVGElement extends Element implements ISVGElement { public focus(): void { HTMLElementUtility.focus(this); } - - /** - * @override - */ - public setAttributeNode(attribute: IAttr): IAttr | null { - const replacedAttribute = super.setAttributeNode(attribute); - - if (attribute.name === 'style' && this._style) { - this._style.cssText = attribute.value; - } - - return replacedAttribute; - } - - /** - * @override - */ - public removeAttributeNode(attribute: IAttr): IAttr { - super.removeAttributeNode(attribute); - - if (attribute.name === 'style' && this._style) { - this._style.cssText = ''; - } - - return attribute; - } } diff --git a/packages/happy-dom/src/nodes/svg-element/SVGElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/svg-element/SVGElementNamedNodeMap.ts new file mode 100644 index 000000000..d2ddd15a0 --- /dev/null +++ b/packages/happy-dom/src/nodes/svg-element/SVGElementNamedNodeMap.ts @@ -0,0 +1,38 @@ +import IAttr from '../attr/IAttr.js'; +import ElementNamedNodeMap from '../element/ElementNamedNodeMap.js'; +import SVGElement from './SVGElement.js'; + +/** + * Named Node Map. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap + */ +export default class SVGElementNamedNodeMap extends ElementNamedNodeMap { + protected _ownerElement: SVGElement; + + /** + * @override + */ + public override setNamedItem(item: IAttr): IAttr | null { + const replacedItem = super.setNamedItem(item); + + if (item.name === 'style' && this._ownerElement._style) { + this._ownerElement._style.cssText = item.value; + } + + return replacedItem || null; + } + + /** + * @override + */ + public override removeNamedItem(name: string): IAttr | null { + const removedItem = super.removeNamedItem(name); + + if (removedItem && removedItem.name === 'style' && this._ownerElement._style) { + this._ownerElement._style.cssText = ''; + } + + return removedItem; + } +} diff --git a/packages/happy-dom/src/query-selector/SelectorItem.ts b/packages/happy-dom/src/query-selector/SelectorItem.ts index 765ff6670..6bec15e60 100644 --- a/packages/happy-dom/src/query-selector/SelectorItem.ts +++ b/packages/happy-dom/src/query-selector/SelectorItem.ts @@ -236,7 +236,7 @@ export default class SelectorItem { let priorityWeight = 0; for (const attribute of this.attributes) { - const elementAttribute = (element)._attributes[attribute.name]; + const elementAttribute = (element).attributes.getNamedItem(attribute.name); if (!elementAttribute) { return null; diff --git a/packages/happy-dom/src/xml-serializer/XMLSerializer.ts b/packages/happy-dom/src/xml-serializer/XMLSerializer.ts index a5e40a4cf..bfb3dc2c9 100644 --- a/packages/happy-dom/src/xml-serializer/XMLSerializer.ts +++ b/packages/happy-dom/src/xml-serializer/XMLSerializer.ts @@ -110,11 +110,12 @@ export default class XMLSerializer { private _getAttributes(element: IElement): string { let attributeString = ''; - if (!(element)._attributes.is && (element)._isValue) { + if (!(element).attributes.getNamedItem('is') && (element)._isValue) { attributeString += ' is="' + (element)._isValue + '"'; } - for (const attribute of Object.values((element)._attributes)) { + for (let i = 0, max = (element).attributes.length; i < max; i++) { + const attribute = (element).attributes[i]; if (attribute.value !== null) { const escapedValue = this._options.escapeEntities ? Entities.escapeText(attribute.value) @@ -122,6 +123,7 @@ export default class XMLSerializer { attributeString += ' ' + attribute.name + '="' + escapedValue + '"'; } } + return attributeString; } } diff --git a/packages/happy-dom/test/named-node-map/NamedNodeMap.test.ts b/packages/happy-dom/test/named-node-map/NamedNodeMap.test.ts index 8cea40a72..61bf32f44 100644 --- a/packages/happy-dom/test/named-node-map/NamedNodeMap.test.ts +++ b/packages/happy-dom/test/named-node-map/NamedNodeMap.test.ts @@ -59,10 +59,10 @@ describe('NamedNodeMap', () => { 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'); + 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'); }); }); @@ -71,10 +71,10 @@ describe('NamedNodeMap', () => { 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'); + 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'); }); }); @@ -83,10 +83,10 @@ describe('NamedNodeMap', () => { 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'); + 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'); }); }); @@ -97,7 +97,9 @@ describe('NamedNodeMap', () => { expect(attributes.getNamedItem('key')).toBe(null); - attributes.setNamedItem(attr); + if (attr) { + attributes.setNamedItem(attr); + } expect(attributes.getNamedItem('key')).toBe(attr); }); @@ -109,8 +111,8 @@ describe('NamedNodeMap', () => { const replaced = attributes.setNamedItem(attr); - expect(replaced.name).toBe('key'); - expect(replaced.value).toBe('value1'); + expect(replaced?.name).toBe('key'); + expect(replaced?.value).toBe('value1'); expect(attributes.getNamedItem('key')).toBe(attr); expect(element.getAttribute('key')).toBe('value2'); }); @@ -121,7 +123,9 @@ describe('NamedNodeMap', () => { element.setAttributeNS('namespace', 'key', 'value'); const attr = attributes.removeNamedItemNS('namespace', 'key'); - attributes.setNamedItemNS(attr); + if (attr) { + attributes.setNamedItemNS(attr); + } expect(attributes.getNamedItem('key')).toBe(attr); expect(element.getAttributeNS('namespace', 'key')).toBe('value'); @@ -134,8 +138,8 @@ describe('NamedNodeMap', () => { const replaced = attributes.setNamedItemNS(attr); - expect(replaced.name).toBe('key'); - expect(replaced.value).toBe('value1'); + expect(replaced?.name).toBe('key'); + expect(replaced?.value).toBe('value1'); expect(attributes.getNamedItemNS('namespace', 'key')).toBe(attr); expect(element.getAttributeNS('namespace', 'key')).toBe('value2'); @@ -147,8 +151,8 @@ describe('NamedNodeMap', () => { element.setAttribute('key', 'value'); const removed = attributes.removeNamedItem('key'); - expect(removed.name).toBe('key'); - expect(removed.value).toBe('value'); + expect(removed?.name).toBe('key'); + expect(removed?.value).toBe('value'); expect(element.getAttribute('key')).toBe(null); }); diff --git a/packages/happy-dom/test/nodes/html-unknown-element/HTMLUnknownElement.test.ts b/packages/happy-dom/test/nodes/html-unknown-element/HTMLUnknownElement.test.ts index 64c17dcbe..9ccde278c 100644 --- a/packages/happy-dom/test/nodes/html-unknown-element/HTMLUnknownElement.test.ts +++ b/packages/happy-dom/test/nodes/html-unknown-element/HTMLUnknownElement.test.ts @@ -51,6 +51,10 @@ describe('HTMLUnknownElement', () => { document.body.appendChild(element); + const attribute1 = document.createAttribute('test'); + attribute1.value = 'test'; + element.attributes.setNamedItem(attribute1); + const childNodes = element.childNodes; const children = element.children; const rootNode = (element._rootNode = document.createElement('div')); @@ -59,7 +63,6 @@ describe('HTMLUnknownElement', () => { const textAreaNode = (element._textAreaNode = document.createElement('div')); const observers = element._observers; const isValue = (element._isValue = 'test'); - const attributes = element._attributes; window.customElements.define('custom-element', CustomElement); @@ -79,7 +82,8 @@ describe('HTMLUnknownElement', () => { expect(customElement._textAreaNode === textAreaNode).toBe(true); expect(customElement._observers === observers).toBe(true); expect(customElement._isValue === isValue).toBe(true); - expect(customElement._attributes === attributes).toBe(true); + expect(customElement.attributes.length).toBe(1); + expect(customElement.attributes[0] === attribute1).toBe(true); }); }); });