Repository: capricorn86/happy-dom (ts)

Issue:
TypeError: selector.includes is not a function when using tippy.js 
First of all, great work on this library! I've attempted to migrate tests in my project from jsdom to happy-dom and I was stunned by the results. Some tests seen over 20x improvement in performance!

However, I am running into some issues, and I am happy to help resolve them in some pull request, but first I'd like to understand if this should be fixed in happy-dom or some other place. Maybe you already have this on your radar.

I am using `tippy.js` (or - React version of it) and I have this error:
```
TypeError: selector.includes is not a function
 ❯ Function.getSelectorParts node_modules/happy-dom/lib/query-selector/QuerySelector.js:133:44
 ❯ Function.querySelectorAll node_modules/happy-dom/lib/query-selector/QuerySelector.js:27:34
 ❯ HTMLDocument.querySelectorAll node_modules/happy-dom/lib/nodes/document/Document.js:266:40
 ❯ getArrayOfElements node_modules/tippy.js/dist/tippy.cjs.js:127:29
 ❯ tippy node_modules/tippy.js/dist/tippy.cjs.js:1595:18
 ❯ node_modules/@tippyjs/react/dist/tippy-react.umd.js:256:24
    254|         }
    255|
    256|         var instance = tippy(element || mutableBox.ref || ssrSafeCreateDiv(), Object.a…
       |                        ^
    257|           plugins: [classNamePlugin].concat(props.plugins || [])
    258|         }));
 ❯ commitHookEffectListMount node_modules/react-dom/cjs/react-dom.development.js:20573:26
 ❯ commitLifeCycles node_modules/react-dom/cjs/react-dom.development.js:20634:11
 ❯ commitLayoutEffects node_modules/react-dom/cjs/react-dom.development.js:23426:7
 ❯ HTMLUnknownElement.callCallback node_modules/react-dom/cjs/react-dom.development.js:3945:14
```

I have a CodeSandbox that recreates this issue here: https://codesandbox.io/s/happy-dom-tippy-test-forked-54v9tg?file=/src/App.test.tsx

As far as I see, the problem happens in this function of tippy.js: https://github.com/atomiks/tippyjs/blob/ad85f6feb79cf6c5853c43bf1b2a50c4fa98e7a1/src/dom-utils.ts#L25
And it might seem to be an issue in this library, but it works fine in jsdom. It seems like in jsdom, the first `if` of this function, so:
```
 if (isElement(value)) {
    return [value];
  }
```
is resolving to true, but it is not when using happy-dom. As a result happy-dom code is then called for `querySelectorAll`, but the selector is not a string, so the call to `includes` fails of course.

In the end, on tippy side, this code is being executed to verify the type of the element: https://github.com/atomiks/tippyjs/blob/ad85f6feb79cf6c5853c43bf1b2a50c4fa98e7a1/src/utils.ts#L29 and calling `toString` using jsdom gives this result: `[object HTMLButtonElement]`, but in happy-dom it's just `[object Object]`.

I assume it might be related to this list of non implemented element classes (https://github.com/capricorn86/happy-dom/blob/117595ed2f7d30d7e99757aafb316ab309b86bfc/packages/happy-dom/src/config/NonImplemenetedElementClasses.ts) and adding the `Button` type should fix that, right?

Pull request:
#540@patch: Add toStringTag in Element class to handle Object.prototype.toString.call
Hi, this PR fixes #540 by adding `Symbol.toStringTag` to `Node` and `NodeList` classes.

Patch:
diff --git a/packages/happy-dom/src/nodes/node/Node.ts b/packages/happy-dom/src/nodes/node/Node.ts
index ae2cc05a..5abb8e41 100644
--- a/packages/happy-dom/src/nodes/node/Node.ts
+++ b/packages/happy-dom/src/nodes/node/Node.ts
@@ -73,6 +73,15 @@ export default class Node extends EventTarget implements INode {
 		this.ownerDocument = (<typeof Node>this.constructor)._ownerDocument;
 	}
 
+	/**
+	 * Returns `Symbol.toStringTag`.
+	 *
+	 * @returns `Symbol.toStringTag`.
+	 */
+	public get [Symbol.toStringTag](): string {
+		return this.constructor.name;
+	}
+
 	/**
 	 * Get text value of children.
 	 *
diff --git a/packages/happy-dom/src/nodes/node/NodeList.ts b/packages/happy-dom/src/nodes/node/NodeList.ts
index 9ba580ed..d920e358 100644
--- a/packages/happy-dom/src/nodes/node/NodeList.ts
+++ b/packages/happy-dom/src/nodes/node/NodeList.ts
@@ -5,6 +5,15 @@ import INode from './INode';
  * Class list.
  */
 export default class NodeList extends Array implements INodeList<INode> {
+	/**
+	 * Returns `Symbol.toStringTag`.
+	 *
+	 * @returns `Symbol.toStringTag`.
+	 */
+	public get [Symbol.toStringTag](): string {
+		return this.constructor.name;
+	}
+
 	/**
 	 * Returns item by index.
 	 *


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
index b14c8955..1ead30fa 100644
--- a/packages/happy-dom/test/nodes/html-button-element/HTMLButtonElement.test.ts
+++ b/packages/happy-dom/test/nodes/html-button-element/HTMLButtonElement.test.ts
@@ -13,6 +13,12 @@ describe('HTMLButtonElement', () => {
 		element = <HTMLButtonElement>document.createElement('button');
 	});
 
+	describe('Object.prototype.toString', () => {
+		it('Returns `[object HTMLButtonElement]`', () => {
+			expect(Object.prototype.toString.call(element)).toBe('[object HTMLButtonElement]');
+		});
+	});
+
 	describe('get value()', () => {
 		it(`Returns the attribute "value".`, () => {
 			element.setAttribute('value', 'VALUE');
diff --git a/packages/happy-dom/test/nodes/html-dialog-element/HTMLDialogElement.test.ts b/packages/happy-dom/test/nodes/html-dialog-element/HTMLDialogElement.test.ts
index 601757b7..b47db336 100644
--- a/packages/happy-dom/test/nodes/html-dialog-element/HTMLDialogElement.test.ts
+++ b/packages/happy-dom/test/nodes/html-dialog-element/HTMLDialogElement.test.ts
@@ -15,6 +15,12 @@ describe('HTMLDialogElement', () => {
 		element = <IHTMLDialogElement>document.createElement('dialog');
 	});
 
+	describe('Object.prototype.toString', () => {
+		it('Returns `[object HTMLDialogElement]`', () => {
+			expect(Object.prototype.toString.call(element)).toBe('[object HTMLDialogElement]');
+		});
+	});
+
 	describe('get open()', () => {
 		it('Should be closed by default', () => {
 			expect(element.open).toBe(false);
diff --git a/packages/happy-dom/test/nodes/html-element/HTMLElement.test.ts b/packages/happy-dom/test/nodes/html-element/HTMLElement.test.ts
index 06dc66de..73b97313 100644
--- a/packages/happy-dom/test/nodes/html-element/HTMLElement.test.ts
+++ b/packages/happy-dom/test/nodes/html-element/HTMLElement.test.ts
@@ -20,6 +20,12 @@ describe('HTMLElement', () => {
 		jest.restoreAllMocks();
 	});
 
+	describe('Object.prototype.toString', () => {
+		it('Returns `[object HTMLElement]`', () => {
+			expect(Object.prototype.toString.call(element)).toBe('[object HTMLElement]');
+		});
+	});
+
 	for (const property of ['accessKey', 'accessKeyLabel']) {
 		describe(`${property}`, () => {
 			it('Returns "".', () => {
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 1332ef90..bd2831d7 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
@@ -13,6 +13,12 @@ describe('HTMLFormElement', () => {
 		element = <HTMLFormElement>document.createElement('form');
 	});
 
+	describe('Object.prototype.toString', () => {
+		it('Returns `[object HTMLFormElement]`', () => {
+			expect(Object.prototype.toString.call(element)).toBe('[object HTMLFormElement]');
+		});
+	});
+
 	for (const property of [
 		'name',
 		'target',
diff --git a/packages/happy-dom/test/nodes/html-image-element/HTMLImageElement.test.ts b/packages/happy-dom/test/nodes/html-image-element/HTMLImageElement.test.ts
index efcc4169..00dc6ae2 100644
--- a/packages/happy-dom/test/nodes/html-image-element/HTMLImageElement.test.ts
+++ b/packages/happy-dom/test/nodes/html-image-element/HTMLImageElement.test.ts
@@ -11,6 +11,13 @@ describe('HTMLImageElement', () => {
 		document = window.document;
 	});
 
+	describe('Object.prototype.toString', () => {
+		it('Returns `[object HTMLImageElement]`', () => {
+			const element = document.createElement('img');
+			expect(Object.prototype.toString.call(element)).toBe('[object HTMLImageElement]');
+		});
+	});
+
 	for (const property of ['alt', 'referrerPolicy', 'sizes', 'src', 'srcset', 'useMap']) {
 		describe(`get ${property}()`, () => {
 			it(`Returns the "${property}" attribute.`, () => {
diff --git a/packages/happy-dom/test/nodes/html-input-element/HTMLInputElement.test.ts b/packages/happy-dom/test/nodes/html-input-element/HTMLInputElement.test.ts
index 9e965738..0c1d020a 100644
--- a/packages/happy-dom/test/nodes/html-input-element/HTMLInputElement.test.ts
+++ b/packages/happy-dom/test/nodes/html-input-element/HTMLInputElement.test.ts
@@ -18,6 +18,12 @@ describe('HTMLInputElement', () => {
 		element = <HTMLInputElement>document.createElement('input');
 	});
 
+	describe('Object.prototype.toString', () => {
+		it('Returns `[object HTMLInputElement]`', () => {
+			expect(Object.prototype.toString.call(element)).toBe('[object HTMLInputElement]');
+		});
+	});
+
 	describe('get value()', () => {
 		for (const type of ['hidden', 'submit', 'image', 'reset', 'button']) {
 			it(`Returns the attribute "value" if type is "${type}".`, () => {
diff --git a/packages/happy-dom/test/nodes/html-label-element/HTMLLabelElement.test.ts b/packages/happy-dom/test/nodes/html-label-element/HTMLLabelElement.test.ts
index 9f1d2e24..2c75934f 100644
--- a/packages/happy-dom/test/nodes/html-label-element/HTMLLabelElement.test.ts
+++ b/packages/happy-dom/test/nodes/html-label-element/HTMLLabelElement.test.ts
@@ -13,6 +13,12 @@ describe('HTMLLabelElement', () => {
 		element = <IHTMLLabelElement>document.createElement('label');
 	});
 
+	describe('Object.prototype.toString', () => {
+		it('Returns `[object HTMLLabelElement]`', () => {
+			expect(Object.prototype.toString.call(element)).toBe('[object HTMLLabelElement]');
+		});
+	});
+
 	describe('get htmlFor()', () => {
 		it('Returns attribute value.', () => {
 			expect(element.htmlFor).toBe('');
diff --git a/packages/happy-dom/test/nodes/html-link-element/HTMLLinkElement.test.ts b/packages/happy-dom/test/nodes/html-link-element/HTMLLinkElement.test.ts
index 5241d5be..84347d87 100644
--- a/packages/happy-dom/test/nodes/html-link-element/HTMLLinkElement.test.ts
+++ b/packages/happy-dom/test/nodes/html-link-element/HTMLLinkElement.test.ts
@@ -17,6 +17,13 @@ describe('HTMLLinkElement', () => {
 		jest.restoreAllMocks();
 	});
 
+	describe('Object.prototype.toString', () => {
+		it('Returns `[object HTMLLinkElement]`', () => {
+			const element = <IHTMLLinkElement>document.createElement('link');
+			expect(Object.prototype.toString.call(element)).toBe('[object HTMLLinkElement]');
+		});
+	});
+
 	for (const property of [
 		'as',
 		'crossOrigin',
diff --git a/packages/happy-dom/test/nodes/html-media-element/HTMLMediaElement.test.ts b/packages/happy-dom/test/nodes/html-media-element/HTMLMediaElement.test.ts
index 58212f97..4f9f8c11 100644
--- a/packages/happy-dom/test/nodes/html-media-element/HTMLMediaElement.test.ts
+++ b/packages/happy-dom/test/nodes/html-media-element/HTMLMediaElement.test.ts
@@ -21,6 +21,12 @@ describe('HTMLMediaElement', () => {
 		jest.restoreAllMocks();
 	});
 
+	describe('Object.prototype.toString', () => {
+		it('Returns `[object HTMLAudioElement]`', () => {
+			expect(Object.prototype.toString.call(element)).toBe('[object HTMLAudioElement]');
+		});
+	});
+
 	for (const property of ['autoplay', 'controls', 'loop']) {
 		describe(`get ${property}()`, () => {
 			it('Returns attribute value.', () => {
diff --git a/packages/happy-dom/test/nodes/html-meta-element/HTMLMetaElement.test.ts b/packages/happy-dom/test/nodes/html-meta-element/HTMLMetaElement.test.ts
index 6c25daa2..61be07e6 100644
--- a/packages/happy-dom/test/nodes/html-meta-element/HTMLMetaElement.test.ts
+++ b/packages/happy-dom/test/nodes/html-meta-element/HTMLMetaElement.test.ts
@@ -13,6 +13,12 @@ describe('HTMLMetaElement', () => {
 		element = <IHTMLMetaElement>document.createElement('meta');
 	});
 
+	describe('Object.prototype.toString', () => {
+		it('Returns `[object HTMLMetaElement]`', () => {
+			expect(Object.prototype.toString.call(element)).toBe('[object HTMLMetaElement]');
+		});
+	});
+
 	describe('get content()', () => {
 		it('Returns attribute value.', () => {
 			expect(element.content).toBe('');
diff --git a/packages/happy-dom/test/nodes/html-opt-group-element/HTMLOptGroupElement.test.ts b/packages/happy-dom/test/nodes/html-opt-group-element/HTMLOptGroupElement.test.ts
index 5ab53e88..55910dbd 100644
--- a/packages/happy-dom/test/nodes/html-opt-group-element/HTMLOptGroupElement.test.ts
+++ b/packages/happy-dom/test/nodes/html-opt-group-element/HTMLOptGroupElement.test.ts
@@ -13,6 +13,12 @@ describe('HTMLOptGroupElement', () => {
 		element = <HTMLOptGroupElement>document.createElement('optgroup');
 	});
 
+	describe('Object.prototype.toString', () => {
+		it('Returns `[object HTMLOptGroupElement]`', () => {
+			expect(Object.prototype.toString.call(element)).toBe('[object HTMLOptGroupElement]');
+		});
+	});
+
 	describe(`get disabled()`, () => {
 		it('Returns attribute value.', () => {
 			expect(element.disabled).toBe(false);
diff --git a/packages/happy-dom/test/nodes/html-option-element/HTMLOptionElement.test.ts b/packages/happy-dom/test/nodes/html-option-element/HTMLOptionElement.test.ts
index b03d10f5..cea28981 100644
--- a/packages/happy-dom/test/nodes/html-option-element/HTMLOptionElement.test.ts
+++ b/packages/happy-dom/test/nodes/html-option-element/HTMLOptionElement.test.ts
@@ -13,6 +13,12 @@ describe('HTMLOptionElement', () => {
 		element = <HTMLOptionElement>document.createElement('option');
 	});
 
+	describe('Object.prototype.toString', () => {
+		it('Returns `[object HTMLOptionElement]`', () => {
+			expect(Object.prototype.toString.call(element)).toBe('[object HTMLOptionElement]');
+		});
+	});
+
 	describe('get value()', () => {
 		it('Returns the attribute "value".', () => {
 			element.setAttribute('value', 'VALUE');
diff --git a/packages/happy-dom/test/nodes/html-script-element/HTMLScriptElement.test.ts b/packages/happy-dom/test/nodes/html-script-element/HTMLScriptElement.test.ts
index 5a17fa1f..68d746b8 100644
--- a/packages/happy-dom/test/nodes/html-script-element/HTMLScriptElement.test.ts
+++ b/packages/happy-dom/test/nodes/html-script-element/HTMLScriptElement.test.ts
@@ -16,6 +16,13 @@ describe('HTMLScriptElement', () => {
 		jest.restoreAllMocks();
 	});
 
+	describe('Object.prototype.toString', () => {
+		it('Returns `[object HTMLScriptElement]`', () => {
+			const element = document.createElement('script');
+			expect(Object.prototype.toString.call(element)).toBe('[object HTMLScriptElement]');
+		});
+	});
+
 	for (const property of ['type', 'charset', 'lang']) {
 		describe(`get ${property}()`, () => {
 			it(`Returns the "${property}" attribute.`, () => {
diff --git a/packages/happy-dom/test/nodes/html-select-element/HTMLSelectElement.test.ts b/packages/happy-dom/test/nodes/html-select-element/HTMLSelectElement.test.ts
index e2923a19..03daf5dd 100644
--- a/packages/happy-dom/test/nodes/html-select-element/HTMLSelectElement.test.ts
+++ b/packages/happy-dom/test/nodes/html-select-element/HTMLSelectElement.test.ts
@@ -13,6 +13,12 @@ describe('HTMLSelectElement', () => {
 		element = <HTMLSelectElement>document.createElement('select');
 	});
 
+	describe('Object.prototype.toString', () => {
+		it('Returns `[object HTMLSelectElement]`', () => {
+			expect(Object.prototype.toString.call(element)).toBe('[object HTMLSelectElement]');
+		});
+	});
+
 	describe('get value()', () => {
 		it('Returns the attribute "value".', () => {});
 	});
diff --git a/packages/happy-dom/test/nodes/html-style-element/HTMLStyleElement.test.ts b/packages/happy-dom/test/nodes/html-style-element/HTMLStyleElement.test.ts
index 6cfcc960..f06e0d76 100644
--- a/packages/happy-dom/test/nodes/html-style-element/HTMLStyleElement.test.ts
+++ b/packages/happy-dom/test/nodes/html-style-element/HTMLStyleElement.test.ts
@@ -17,6 +17,12 @@ describe('HTMLStyleElement', () => {
 		jest.restoreAllMocks();
 	});
 
+	describe('Object.prototype.toString', () => {
+		it('Returns `[object HTMLStyleElement]`', () => {
+			expect(Object.prototype.toString.call(element)).toBe('[object HTMLStyleElement]');
+		});
+	});
+
 	for (const property of ['media', 'type']) {
 		describe(`get ${property}()`, () => {
 			it(`Returns the "${property}" attribute.`, () => {
diff --git a/packages/happy-dom/test/nodes/html-template-element/HTMLTemplateElement.test.ts b/packages/happy-dom/test/nodes/html-template-element/HTMLTemplateElement.test.ts
index 4fcd54d7..e5f804d6 100644
--- a/packages/happy-dom/test/nodes/html-template-element/HTMLTemplateElement.test.ts
+++ b/packages/happy-dom/test/nodes/html-template-element/HTMLTemplateElement.test.ts
@@ -18,6 +18,12 @@ describe('HTMLTemplateElement', () => {
 		jest.restoreAllMocks();
 	});
 
+	describe('Object.prototype.toString', () => {
+		it('Returns `[object HTMLTemplateElement]`', () => {
+			expect(Object.prototype.toString.call(element)).toBe('[object HTMLTemplateElement]');
+		});
+	});
+
 	describe('get innerHTML()', () => {
 		it('Returns inner HTML of the "content" node.', () => {
 			const div = document.createElement('div');
diff --git a/packages/happy-dom/test/nodes/html-text-area-element/HTMLTextAreaElement.test.ts b/packages/happy-dom/test/nodes/html-text-area-element/HTMLTextAreaElement.test.ts
index d28e7dd7..e49f1652 100644
--- a/packages/happy-dom/test/nodes/html-text-area-element/HTMLTextAreaElement.test.ts
+++ b/packages/happy-dom/test/nodes/html-text-area-element/HTMLTextAreaElement.test.ts
@@ -15,6 +15,12 @@ describe('HTMLTextAreaElement', () => {
 		element = <HTMLTextAreaElement>document.createElement('textarea');
 	});
 
+	describe('Object.prototype.toString', () => {
+		it('Returns `[object HTMLTextAreaElement]`', () => {
+			expect(Object.prototype.toString.call(element)).toBe('[object HTMLTextAreaElement]');
+		});
+	});
+
 	describe('get value()', () => {
 		it('Returns the attribute "value" if it has not been set using the property.', () => {
 			element.setAttribute('value', 'TEST_VALUE');