Repository: capricorn86/happy-dom
Pull request #581 (2022-09-15 20:38:40)

Add support for HTMLButtonElement.
When writing some tests, I noticed this:

```
const el = document.createElement("button")
el.setAttribute("disabled", "")
console.log(el.disabled)  // <-- Prints "undefined"
```

In a real browser, the last line would print out `true`. Checked in Chrome and FireFox.

After digging a bit, I realized that `happy-dom` uses the generic `HTMLElement` to represent a `<button>`, which doesn't translate between [standard button attributes and properties](https://developer.mozilla.org/en-US/docs/Web/API/HTMLButtonElement).

This PR adds a `HTMLButtonElement` class and makes it translate some (not all) of the properties - specifically, `value`, `disabled`, and `type`.

capricorn86: Thank you for contributing @fsoikin! 🙂 

Sorry for not looking into this PR earlier. There has been a lot in my private and work life.

Patch:
diff --git a/packages/happy-dom/src/config/ElementTag.ts b/packages/happy-dom/src/config/ElementTag.ts
index 92ba25b9..3080d5ae 100644
--- a/packages/happy-dom/src/config/ElementTag.ts
+++ b/packages/happy-dom/src/config/ElementTag.ts
@@ -17,6 +17,7 @@ import HTMLSelectElement from '../nodes/html-select-element/HTMLSelectElement';
 import HTMLOptionElement from '../nodes/html-option-element/HTMLOptionElement';
 import HTMLOptGroupElement from '../nodes/html-opt-group-element/HTMLOptGroupElement';
 import HTMLDialogElement from '../nodes/html-dialog-element/HTMLDialogElement';
+import HTMLButtonElement from '../nodes/html-button-element/HTMLButtonElement';
 
 export default {
 	A: HTMLElement,
@@ -55,7 +56,7 @@ export default {
 	META: HTMLMetaElement,
 	BLOCKQUOTE: HTMLElement,
 	BR: HTMLElement,
-	BUTTON: HTMLElement,
+	BUTTON: HTMLButtonElement,
 	CANVAS: HTMLElement,
 	CAPTION: HTMLElement,
 	CITE: HTMLElement,
diff --git a/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElement.ts b/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElement.ts
new file mode 100644
index 00000000..c91cc159
--- /dev/null
+++ b/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElement.ts
@@ -0,0 +1,86 @@
+import HTMLElement from '../html-element/HTMLElement';
+import IHTMLButtonElement from './IHTMLButtonElement';
+
+const BUTTON_TYPES = ['submit', 'reset', 'button', 'menu'];
+
+/**
+ 
+We can improve performance a bit if we make the types as a constant.
+ * HTML Button Element.
+ *
+ * Reference:
+ * https://developer.mozilla.org/en-US/docs/Web/API/HTMLButtonElement.
+ */
+export default class HTMLButtonElement extends HTMLElement implements IHTMLButtonElement {
+	/**
+	 * Returns value.
+	 *
+	 * @returns Value.
+	 */
+	public get value(): string {
+		return this.getAttributeNS(null, 'value');
+	}
+
+	/**
+	 * Sets value.
+	 *
+	 * @param value Value.
+	 */
+	public set value(value: string) {
+		this.setAttributeNS(null, 'value', value);
+	}
+
+	/**
+	 * Returns disabled.
+	 *
+	 * @returns Disabled.
+	 */
+	public get disabled(): boolean {
+		return this.getAttributeNS(null, 'disabled') !== null;
+	}
+
+	/**
+	 * Sets disabled.
+	 *
+	 * @param disabled Disabled.
+	 */
+	public set disabled(disabled: boolean) {
+		if (!disabled) {
+			this.removeAttributeNS(null, 'disabled');
+		} else {
+			this.setAttributeNS(null, 'disabled', '');
+		}
+	}
+
+	/**
+	 * Returns type
+	 *
+	 * @returns Type
+	 */
+	public get type(): string {
+		return this._sanitizeType(this.getAttributeNS(null, 'type'));
+	}
+
+	/**
+	 * Sets type
+	 *
+	 * @param v Type
+	 */
+	public set type(v: string) {
+		this.setAttributeNS(null, 'type', this._sanitizeType(v));
+	}
+
+	/**
+	 *
+	 * @param type
+	 */
+	protected _sanitizeType(type: string): string {
+		type = (type && type.toLowerCase()) || 'submit';
+
+		if (!BUTTON_TYPES.includes(type)) {
+			type = 'submit';
+		}
+
+		return type;
+	}
+}
diff --git a/packages/happy-dom/src/nodes/html-button-element/IHTMLButtonElement.ts b/packages/happy-dom/src/nodes/html-button-element/IHTMLButtonElement.ts
new file mode 100644
index 00000000..e92f8e15
--- /dev/null
+++ b/packages/happy-dom/src/nodes/html-button-element/IHTMLButtonElement.ts
@@ -0,0 +1,13 @@
+import IHTMLElement from '../html-element/IHTMLElement';
+
+/**
+ * HTML Button Element.
+ *
+ * Reference:
+ * https://developer.mozilla.org/en-US/docs/Web/API/HTMLButtonElement.
+ */
+export default interface IHTMLButtonElement extends IHTMLElement {
+	type: string;
+	disabled: boolean;
+	value: string;
+}


Test patch:
diff --git a/packages/happy-dom/test/nodes/html-button-element/HTMLButtonElement.test.ts b/packages/happy-dom/test/nodes/html-button-element/HTMLButtonElement.test.ts
new file mode 100644
index 00000000..b14c8955
--- /dev/null
+++ b/packages/happy-dom/test/nodes/html-button-element/HTMLButtonElement.test.ts
@@ -0,0 +1,91 @@
+import Window from '../../../src/window/Window';
+import Document from '../../../src/nodes/document/Document';
+import HTMLButtonElement from '../../../src/nodes/html-button-element/HTMLButtonElement';
+
+describe('HTMLButtonElement', () => {
+	let window: Window;
+	let document: Document;
+	let element: HTMLButtonElement;
+
+	beforeEach(() => {
+		window = new Window();
+		document = window.document;
+		element = <HTMLButtonElement>document.createElement('button');
+	});
+
+	describe('get value()', () => {
+		it(`Returns the attribute "value".`, () => {
+			element.setAttribute('value', 'VALUE');
+			expect(element.value).toBe('VALUE');
+		});
+	});
+
+	describe('set value()', () => {
+		it(`Sets the attribute "value".`, () => {
+			element.value = 'VALUE';
+			expect(element.getAttribute('value')).toBe('VALUE');
+		});
+	});
+
+	describe(`get disabled()`, () => {
+		it('Returns attribute value.', () => {
+			expect(element.disabled).toBe(false);
+			element.setAttribute('disabled', '');
+			expect(element.disabled).toBe(true);
+		});
+	});
+
+	describe(`set disabled()`, () => {
+		it('Sets attribute value.', () => {
+			element.disabled = true;
+			expect(element.getAttribute('disabled')).toBe('');
+		});
+	});
+
+	describe('get type()', () => {
+		it(`Defaults to "submit".`, () => {
+			expect(element.type).toBe('submit');
+		});
+
+		it(`Returns the attribute "type".`, () => {
+			element.setAttribute('type', 'menu');
+			expect(element.type).toBe('menu');
+		});
+
+		it(`Sanitizes the value before returning.`, () => {
+			element.setAttribute('type', 'reset');
+			expect(element.type).toBe('reset');
+
+			element.setAttribute('type', 'button');
+			expect(element.type).toBe('button');
+
+			element.setAttribute('type', 'submit');
+			expect(element.type).toBe('submit');
+
+			element.setAttribute('type', 'MeNu');
+			expect(element.type).toBe('menu');
+
+			element.setAttribute('type', 'foobar');
+			expect(element.type).toBe('submit');
+		});
+	});
+
+	describe('set type()', () => {
+		it(`Sets the attribute "type" after sanitizing.`, () => {
+			element.type = 'SuBmIt';
+			expect(element.getAttribute('type')).toBe('submit');
+
+			element.type = 'reset';
+			expect(element.getAttribute('type')).toBe('reset');
+
+			element.type = 'button';
+			expect(element.getAttribute('type')).toBe('button');
+
+			element.type = 'menu';
+			expect(element.getAttribute('type')).toBe('menu');
+
+			element.type = null;
+			expect(element.getAttribute('type')).toBe('submit');
+		});
+	});
+});