From 1711be408e2d4e3a4478dc324e6d19179dac76ab Mon Sep 17 00:00:00 2001 From: David Ortner Date: Tue, 12 Mar 2024 01:28:10 +0100 Subject: [PATCH] fix: [#1258] Adds support for constructing custom element using new keyword --- .../custom-element/CustomElementRegistry.ts | 43 +++++++++++++++++++ .../happy-dom/src/nodes/document/Document.ts | 5 +-- packages/happy-dom/src/nodes/node/Node.ts | 2 +- .../happy-dom/src/window/BrowserWindow.ts | 6 ++- .../CustomElementRegistry.test.ts | 13 +++++- 5 files changed, 62 insertions(+), 7 deletions(-) diff --git a/packages/happy-dom/src/custom-element/CustomElementRegistry.ts b/packages/happy-dom/src/custom-element/CustomElementRegistry.ts index 133344a8b..8a2f51907 100644 --- a/packages/happy-dom/src/custom-element/CustomElementRegistry.ts +++ b/packages/happy-dom/src/custom-element/CustomElementRegistry.ts @@ -2,6 +2,8 @@ import DOMException from '../exception/DOMException.js'; import * as PropertySymbol from '../PropertySymbol.js'; import HTMLElement from '../nodes/html-element/HTMLElement.js'; import Node from '../nodes/node/Node.js'; +import IBrowserWindow from '../window/IBrowserWindow.js'; +import NamespaceURI from '../config/NamespaceURI.js'; /** * Custom elements registry. @@ -12,6 +14,16 @@ export default class CustomElementRegistry { } = {}; public [PropertySymbol.registedClass]: Map = new Map(); public [PropertySymbol.callbacks]: { [k: string]: (() => void)[] } = {}; + #window: IBrowserWindow; + + /** + * Constructor. + * + * @param window Window. + */ + constructor(window: IBrowserWindow) { + this.#window = window; + } /** * Defines a custom element class. @@ -44,6 +56,25 @@ export default class CustomElementRegistry { ); } + const tagName = name.toUpperCase(); + + elementClass[PropertySymbol.ownerDocument] = this.#window.document; + + Object.defineProperty(elementClass.prototype, 'localName', { + configurable: true, + get: () => name + }); + + Object.defineProperty(elementClass.prototype, 'tagName', { + configurable: true, + get: () => tagName + }); + + Object.defineProperty(elementClass.prototype, 'namespaceURI', { + configurable: true, + get: () => NamespaceURI.html + }); + this[PropertySymbol.registry][name] = { elementClass, extends: options && options.extends ? options.extends.toLowerCase() : null @@ -113,6 +144,18 @@ export default class CustomElementRegistry { return this[PropertySymbol.registedClass].get(elementClass) || null; } + /** + * Destroys the registry. + */ + public [PropertySymbol.destroy](): void { + for (const entity of Object.values(this[PropertySymbol.registry])) { + entity.elementClass[PropertySymbol.ownerDocument] = null; + } + this[PropertySymbol.registry] = {}; + this[PropertySymbol.registedClass] = new Map(); + this[PropertySymbol.callbacks] = {}; + } + /** * Validates the correctness of custom element tag names. * diff --git a/packages/happy-dom/src/nodes/document/Document.ts b/packages/happy-dom/src/nodes/document/Document.ts index cc148da74..3d514ed82 100644 --- a/packages/happy-dom/src/nodes/document/Document.ts +++ b/packages/happy-dom/src/nodes/document/Document.ts @@ -1122,10 +1122,7 @@ export default class Document extends Node implements IDocument { ]; if (customElement) { - const element = NodeFactory.createNode(this, customElement.elementClass); - element[PropertySymbol.tagName] = qualifiedName.toUpperCase(); - element[PropertySymbol.localName] = qualifiedName; - element[PropertySymbol.namespaceURI] = namespaceURI; + const element = new customElement.elementClass(); element[PropertySymbol.isValue] = options && options.is ? String(options.is) : null; return element; } diff --git a/packages/happy-dom/src/nodes/node/Node.ts b/packages/happy-dom/src/nodes/node/Node.ts index 13b52aaf1..4a74db04f 100644 --- a/packages/happy-dom/src/nodes/node/Node.ts +++ b/packages/happy-dom/src/nodes/node/Node.ts @@ -297,7 +297,7 @@ export default class Node extends EventTarget implements INode { * @param otherNode Node to test with. * @returns "true" if this node contains the other node. */ - public contains(otherNode: INode | undefined): boolean { + public contains(otherNode: INode): boolean { if (otherNode === undefined) { return false; } diff --git a/packages/happy-dom/src/window/BrowserWindow.ts b/packages/happy-dom/src/window/BrowserWindow.ts index 50a51454a..7e15cb3f9 100644 --- a/packages/happy-dom/src/window/BrowserWindow.ts +++ b/packages/happy-dom/src/window/BrowserWindow.ts @@ -520,7 +520,7 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow this.#browserFrame = browserFrame; - this.customElements = new CustomElementRegistry(); + this.customElements = new CustomElementRegistry(this); this.navigator = new Navigator(this); this.history = new History(); this.screen = new Screen(); @@ -1274,6 +1274,10 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow this.document.removeChild(node); } + if (this.customElements[PropertySymbol.destroy]) { + this.customElements[PropertySymbol.destroy](); + } + this.document[PropertySymbol.activeElement] = null; this.document[PropertySymbol.nextActiveElement] = null; this.document[PropertySymbol.currentScript] = null; diff --git a/packages/happy-dom/test/custom-element/CustomElementRegistry.test.ts b/packages/happy-dom/test/custom-element/CustomElementRegistry.test.ts index a7eb907d4..c27c1e1a1 100644 --- a/packages/happy-dom/test/custom-element/CustomElementRegistry.test.ts +++ b/packages/happy-dom/test/custom-element/CustomElementRegistry.test.ts @@ -6,6 +6,7 @@ import Window from '../../src/window/Window.js'; import DOMException from '../../src/exception/DOMException.js'; import { beforeEach, describe, it, expect } from 'vitest'; import * as PropertySymbol from '../../src/PropertySymbol.js'; +import NamespaceURI from '../../src/config/NamespaceURI.js'; describe('CustomElementRegistry', () => { let customElements; @@ -15,7 +16,7 @@ describe('CustomElementRegistry', () => { beforeEach(() => { window = new Window(); document = window.document; - customElements = new CustomElementRegistry(); + customElements = new CustomElementRegistry(window); CustomElement.observedAttributesCallCount = 0; }); @@ -37,6 +38,16 @@ describe('CustomElementRegistry', () => { expect(customElements[PropertySymbol.registry]['custom-element'].extends).toBe('ul'); }); + it('Can construct CustomElement instance using "new".', () => { + customElements.define('custom-element', CustomElement); + const customElement = new CustomElement(); + expect(customElement).toBeInstanceOf(CustomElement); + expect(customElement.ownerDocument).toBe(document); + expect(customElement.localName).toBe('custom-element'); + expect(customElement.tagName).toBe('CUSTOM-ELEMENT'); + expect(customElement.namespaceURI).toBe(NamespaceURI.html); + }); + it('Throws an error if tag name does not contain "-".', () => { expect(() => customElements.define('element', CustomElement)).toThrow( new DOMException(