From 6f4e4708d02345900c81b3190266c5fc71786118 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Tue, 2 May 2023 17:32:02 +0200 Subject: [PATCH] #875@patch: Fixes issue in HTMLCollection and HTMLFormControlsCollection related to id/name of elements colliding with class properties and methods. --- .../src/nodes/element/HTMLCollection.ts | 10 ++- .../HTMLFormControlsCollection.ts | 8 +- .../test/nodes/element/HTMLCollection.test.ts | 40 ++++++++++ .../html-form-element/HTMLFormElement.test.ts | 73 +++++++++++++++++++ 4 files changed, 126 insertions(+), 5 deletions(-) diff --git a/packages/happy-dom/src/nodes/element/HTMLCollection.ts b/packages/happy-dom/src/nodes/element/HTMLCollection.ts index b9b6ac147..eb66f68e6 100644 --- a/packages/happy-dom/src/nodes/element/HTMLCollection.ts +++ b/packages/happy-dom/src/nodes/element/HTMLCollection.ts @@ -41,7 +41,7 @@ export default class HTMLCollection extends Array implements IHTMLCollection< this._namedItems[name].push(node); } - if (!this[name] && this._isValidPropertyName(name)) { + if (!this.hasOwnProperty(name) && this._isValidPropertyName(name)) { this[name] = this._namedItems[name][0]; } } @@ -62,7 +62,7 @@ export default class HTMLCollection extends Array implements IHTMLCollection< if (this._namedItems[name].length === 0) { delete this._namedItems[name]; - if (this[name] && this._isValidPropertyName(name)) { + if (this.hasOwnProperty(name) && this._isValidPropertyName(name)) { delete this[name]; } } else if (this._isValidPropertyName(name)) { @@ -79,6 +79,10 @@ export default class HTMLCollection extends Array implements IHTMLCollection< * @returns True if the property name is valid. */ protected _isValidPropertyName(name: string): boolean { - return isNaN(Number(name)) || name.includes('.'); + return ( + !this.constructor.prototype.hasOwnProperty(name) && + !Array.prototype.hasOwnProperty(name) && + (isNaN(Number(name)) || name.includes('.')) + ); } } diff --git a/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts b/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts index a45a78601..338abb1c8 100644 --- a/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts +++ b/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts @@ -93,7 +93,7 @@ export default class HTMLFormControlsCollection if (this._namedItems[name].length === 0) { delete this._namedItems[name]; - if (this[name] && this._isValidPropertyName(name)) { + if (this.hasOwnProperty(name) && this._isValidPropertyName(name)) { delete this[name]; } } else if (this._isValidPropertyName(name)) { @@ -111,6 +111,10 @@ export default class HTMLFormControlsCollection * @returns True if the property name is valid. */ protected _isValidPropertyName(name: string): boolean { - return isNaN(Number(name)) || name.includes('.'); + return ( + !this.constructor.prototype.hasOwnProperty(name) && + !Array.prototype.hasOwnProperty(name) && + (isNaN(Number(name)) || name.includes('.')) + ); } } diff --git a/packages/happy-dom/test/nodes/element/HTMLCollection.test.ts b/packages/happy-dom/test/nodes/element/HTMLCollection.test.ts index fb150ece0..e0fd70979 100644 --- a/packages/happy-dom/test/nodes/element/HTMLCollection.test.ts +++ b/packages/happy-dom/test/nodes/element/HTMLCollection.test.ts @@ -117,5 +117,45 @@ describe('HTMLCollection', () => { expect(div.children.namedItem('0') === container3).toBe(true); expect(div.children.namedItem('1') === container4).toBe(true); }); + + it('Supports attributes that has the same name as properties and methods of the HTMLCollection class.', () => { + const div = document.createElement('div'); + div.innerHTML = `
`; + const container1 = div.querySelector('.container1'); + const container2 = div.querySelector('.container2'); + const container3 = div.querySelector('.container3'); + + expect(div.children.length).toBe(3); + expect(div.children[0] === container1).toBe(true); + expect(div.children[1] === container2).toBe(true); + expect(div.children[2] === container3).toBe(true); + expect(div.children.namedItem('length') === container1).toBe(true); + expect(div.children.namedItem('namedItem') === container2).toBe(true); + expect(div.children.namedItem('push') === container3).toBe(true); + + expect(typeof div.children['namedItem']).toBe('function'); + expect(typeof div.children['push']).toBe('function'); + + container2.remove(); + + expect(div.children.length).toBe(2); + expect(div.children[0] === container1).toBe(true); + expect(div.children[1] === container3).toBe(true); + expect(div.children.namedItem('length') === container1).toBe(true); + expect(div.children.namedItem('push') === container3).toBe(true); + + div.insertBefore(container2, container3); + + expect(div.children.length).toBe(3); + expect(div.children[0] === container1).toBe(true); + expect(div.children[1] === container2).toBe(true); + expect(div.children[2] === container3).toBe(true); + expect(div.children.namedItem('length') === container1).toBe(true); + expect(div.children.namedItem('namedItem') === container2).toBe(true); + expect(div.children.namedItem('push') === container3).toBe(true); + + expect(typeof div.children['namedItem']).toBe('function'); + expect(typeof div.children['push']).toBe('function'); + }); }); }); diff --git a/packages/happy-dom/test/nodes/html-form-element/HTMLFormElement.test.ts b/packages/happy-dom/test/nodes/html-form-element/HTMLFormElement.test.ts index f56301ad3..274493bce 100644 --- a/packages/happy-dom/test/nodes/html-form-element/HTMLFormElement.test.ts +++ b/packages/happy-dom/test/nodes/html-form-element/HTMLFormElement.test.ts @@ -267,6 +267,79 @@ describe('HTMLFormElement', () => { expect(elements['checkbox1'] === undefined).toBe(true); expect(elements['radio1'] === undefined).toBe(true); }); + + it('Returns control elements using the same name as properties and methods of the HTMLCollection class.', () => { + element.innerHTML = ` +
+ + + + + +
+ `; + const elements = element.elements; + const root = element.children[0]; + + expect(element.length).toBe(5); + expect(elements.length).toBe(5); + + expect(element[0] === root.children[0]).toBe(true); + expect(element[1] === root.children[1]).toBe(true); + expect(element[2] === root.children[2]).toBe(true); + expect(element[3] === root.children[3]).toBe(true); + expect(element[4] === root.children[4]).toBe(true); + + expect(elements[0] === root.children[0]).toBe(true); + expect(elements[1] === root.children[1]).toBe(true); + expect(elements[2] === root.children[2]).toBe(true); + expect(elements[3] === root.children[3]).toBe(true); + expect(elements[4] === root.children[4]).toBe(true); + + expect(elements.item(0) === root.children[0]).toBe(true); + expect(elements.item(1) === root.children[1]).toBe(true); + expect(elements.item(2) === root.children[2]).toBe(true); + expect(elements.item(3) === root.children[3]).toBe(true); + expect(elements.item(4) === root.children[4]).toBe(true); + + const radioNodeList = new RadioNodeList(); + radioNodeList.push(root.children[1]); + radioNodeList.push(root.children[2]); + radioNodeList.push(root.children[3]); + + expect(typeof elements.push).toBe('function'); + expect(typeof elements.namedItem).toBe('function'); + expect(elements.namedItem('length') === root.children[0]).toBe(true); + expect(elements.namedItem('namedItem')).toEqual(radioNodeList); + expect(elements.namedItem('push') === root.children[4]).toBe(true); + + const children = root.children.slice(); + + for (const child of children) { + root.removeChild(child); + } + + expect(element.length).toBe(0); + + for (const child of children) { + root.appendChild(child); + } + + expect(element.length).toBe(5); + expect(elements.length).toBe(5); + + expect(elements[0] === root.children[0]).toBe(true); + expect(elements[1] === root.children[1]).toBe(true); + expect(elements[2] === root.children[2]).toBe(true); + expect(elements[3] === root.children[3]).toBe(true); + expect(elements[4] === root.children[4]).toBe(true); + + expect(typeof elements.push).toBe('function'); + expect(typeof elements.namedItem).toBe('function'); + expect(elements.namedItem('length') === root.children[0]).toBe(true); + expect(elements.namedItem('namedItem')).toEqual(radioNodeList); + expect(elements.namedItem('push') === root.children[4]).toBe(true); + }); }); describe('submit()', () => {